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.9.0
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.10.0
Choose a head ref
  • 5 commits
  • 17 files changed
  • 4 contributors

Commits on Jan 15, 2022

  1. add badge

    epoberezkin committed Jan 15, 2022
    Copy the full SHA
    8b993dc View commit details

Commits on Feb 4, 2022

  1. feat: add uriresolver option (#1862)

    * feat: add uriresolver option
    
    * fix: review
    
    * fix: prettier
    
    * fix: lint
    
    * fix: param order
    
    * fix: test
    
    * fix: test
    
    * fix: test
    
    * fix: order
    
    * feat: bump version
    
    * feat: bump version
    
    Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
    zekth and epoberezkin authored Feb 4, 2022
    Copy the full SHA
    0e47ab4 View commit details
  2. Copy the full SHA
    b3e0cb1 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d0be809 View commit details
  4. 8.10.0

    epoberezkin committed Feb 4, 2022
    Copy the full SHA
    a27f782 View commit details
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ Supports JSON Schema draft-04/06/07/2019-09/2020-12 ([draft-04 support](https://
[![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)
[![Coverage Status](https://coveralls.io/repos/github/ajv-validator/ajv/badge.svg?branch=master)](https://coveralls.io/github/ajv-validator/ajv?branch=master)
[![SimpleX](https://img.shields.io/badge/chat-on%20SimpleX-%2307b4b9)](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fap4lMFzfXF8Hzmh-Vz0WNxp_1jKiOa-h%23MCowBQYDK2VuAyEAcdefddRvDfI8iAuBpztm_J3qFucj8MDZoVs_2EcMTzU%3D)
[![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)

2 changes: 1 addition & 1 deletion docs/guide/typescript.md
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
Ajv takes advantage of TypeScript type system to provide additional functionality that is not possible in JavaScript:

- utility types `JSONSchemaType` and `JTDSchemaType` to convert data type into the schema type to simplify writing schemas, both for [JSON Schema](../json-schema.md) (but without union support) and for [JSON Type Definition](../json-type-definition) (with tagged unions support).
- utility type `JTDDataType` to covert JSON Type Definition schema into the type of data that it defines.
- utility type `JTDDataType` to convert JSON Type Definition schema into the type of data that it defines.
- compiled validation functions are type guards that narrow the type after successful validation.
- validation errors for JSON Schema are defined as tagged unions, for type-safe error handling.
- when utility type is used, compiled JTD serializers only accept data of correct type (as they do not validate that the data is valid) and compiled parsers return correct data type.
2 changes: 1 addition & 1 deletion docs/json-schema.md
Original file line number Diff line number Diff line change
@@ -991,7 +991,7 @@ From the perspective of validation result `discriminator` is defined as no-op (t
There are following requirements and limitations of using `discriminator` keyword:
- `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.
- discriminator property should be [required](#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. 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.

16 changes: 8 additions & 8 deletions lib/compile/index.ts
Original file line number Diff line number Diff line change
@@ -112,7 +112,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv {
// TODO refactor - remove compilations
const _sch = getCompilingSchema.call(this, sch)
if (_sch) return _sch
const rootId = getFullPath(sch.root.baseId) // TODO if getFullPath removed 1 tests fails
const rootId = getFullPath(this.opts.uriResolver, sch.root.baseId) // TODO if getFullPath removed 1 tests fails
const {es5, lines} = this.opts.code
const {ownProperties} = this.opts
const gen = new CodeGen(this.scope, {es5, lines, ownProperties})
@@ -208,7 +208,7 @@ export function resolveRef(
baseId: string,
ref: string
): AnySchema | SchemaEnv | undefined {
ref = resolveUrl(baseId, ref)
ref = resolveUrl(this.opts.uriResolver, baseId, ref)
const schOrFunc = root.refs[ref]
if (schOrFunc) return schOrFunc

@@ -257,9 +257,9 @@ export function resolveSchema(
root: SchemaEnv, // root object with properties schema, refs TODO below SchemaEnv is assigned to it
ref: string // reference to resolve
): SchemaEnv | undefined {
const p = URI.parse(ref)
const refPath = _getFullPath(p)
let baseId = getFullPath(root.baseId)
const p = this.opts.uriResolver.parse(ref)
const refPath = _getFullPath(this.opts.uriResolver, p)
let baseId = getFullPath(this.opts.uriResolver, root.baseId, undefined)
// TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
if (Object.keys(root.schema).length > 0 && refPath === baseId) {
return getJsonPointer.call(this, p, root)
@@ -279,7 +279,7 @@ export function resolveSchema(
const {schema} = schOrRef
const {schemaId} = this.opts
const schId = schema[schemaId]
if (schId) baseId = resolveUrl(baseId, schId)
if (schId) baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
return new SchemaEnv({schema, schemaId, root, baseId})
}
return getJsonPointer.call(this, p, schOrRef)
@@ -307,12 +307,12 @@ function getJsonPointer(
// TODO PREVENT_SCOPE_CHANGE could be defined in keyword def?
const schId = typeof schema === "object" && schema[this.opts.schemaId]
if (!PREVENT_SCOPE_CHANGE.has(part) && schId) {
baseId = resolveUrl(baseId, schId)
baseId = resolveUrl(this.opts.uriResolver, baseId, schId)
}
}
let env: SchemaEnv | undefined
if (typeof schema != "boolean" && schema.$ref && !schemaHasRulesButRef(schema, this.RULES)) {
const $ref = resolveUrl(baseId, schema.$ref)
const $ref = resolveUrl(this.opts.uriResolver, baseId, schema.$ref)
env = resolveSchema.call(this, root, $ref)
}
// even though resolution failed we need to return SchemaEnv to throw exception
2 changes: 1 addition & 1 deletion lib/compile/jtd/parse.ts
Original file line number Diff line number Diff line change
@@ -342,7 +342,7 @@ function parseRef(cxt: ParseCxt): void {
const {gen, self, definitions, schema, schemaEnv} = cxt
const {ref} = schema
const refSchema = definitions[ref]
if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
if (!hasRef(refSchema)) return parseCode({...cxt, schema: refSchema})
const {root} = schemaEnv
const sch = compileParser.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
2 changes: 1 addition & 1 deletion lib/compile/jtd/serialize.ts
Original file line number Diff line number Diff line change
@@ -234,7 +234,7 @@ function serializeRef(cxt: SerializeCxt): void {
const {gen, self, data, definitions, schema, schemaEnv} = cxt
const {ref} = schema
const refSchema = definitions[ref]
if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
if (!refSchema) throw new MissingRefError(self.opts.uriResolver, "", ref, `No definition ${ref}`)
if (!hasRef(refSchema)) return serializeCode({...cxt, schema: refSchema})
const {root} = schemaEnv
const sch = compileSerializer.call(self, new SchemaEnv({schema: refSchema, root}), definitions)
7 changes: 4 additions & 3 deletions lib/compile/ref_error.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {resolveUrl, normalizeId, getFullPath} from "./resolve"
import type {UriResolver} from "../types"

export default class MissingRefError extends Error {
readonly missingRef: string
readonly missingSchema: string

constructor(baseId: string, ref: string, msg?: string) {
constructor(resolver: UriResolver, baseId: string, ref: string, msg?: string) {
super(msg || `can't resolve reference ${ref} from id ${baseId}`)
this.missingRef = resolveUrl(baseId, ref)
this.missingSchema = normalizeId(getFullPath(this.missingRef))
this.missingRef = resolveUrl(resolver, baseId, ref)
this.missingSchema = normalizeId(getFullPath(resolver, this.missingRef))
}
}
27 changes: 15 additions & 12 deletions lib/compile/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type {AnySchema, AnySchemaObject} from "../types"
import type {AnySchema, AnySchemaObject, UriResolver} from "../types"
import type Ajv from "../ajv"
import type {URIComponents} from "uri-js"
import {eachItem} from "./util"
import * as equal from "fast-deep-equal"
import * as traverse from "json-schema-traverse"
import * as URI from "uri-js"

// the hash of local references inside the schema (created by getSchemaRefs), used for inline resolution
export type LocalRefs = {[Ref in string]?: AnySchemaObject}
@@ -67,34 +67,35 @@ function countKeys(schema: AnySchemaObject): number {
return count
}

export function getFullPath(id = "", normalize?: boolean): string {
export function getFullPath(resolver: UriResolver, id = "", normalize?: boolean): string {
if (normalize !== false) id = normalizeId(id)
const p = URI.parse(id)
return _getFullPath(p)
const p = resolver.parse(id)
return _getFullPath(resolver, p)
}

export function _getFullPath(p: URI.URIComponents): string {
return URI.serialize(p).split("#")[0] + "#"
export function _getFullPath(resolver: UriResolver, p: URIComponents): string {
const serialized = resolver.serialize(p)
return serialized.split("#")[0] + "#"
}

const TRAILING_SLASH_HASH = /#\/?$/
export function normalizeId(id: string | undefined): string {
return id ? id.replace(TRAILING_SLASH_HASH, "") : ""
}

export function resolveUrl(baseId: string, id: string): string {
export function resolveUrl(resolver: UriResolver, baseId: string, id: string): string {
id = normalizeId(id)
return URI.resolve(baseId, id)
return resolver.resolve(baseId, id)
}

const ANCHOR = /^[a-z_][-a-z0-9._]*$/i

export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): LocalRefs {
if (typeof schema == "boolean") return {}
const {schemaId} = this.opts
const {schemaId, uriResolver} = this.opts
const schId = normalizeId(schema[schemaId] || baseId)
const baseIds: {[JsonPtr in string]?: string} = {"": schId}
const pathPrefix = getFullPath(schId, false)
const pathPrefix = getFullPath(uriResolver, schId, false)
const localRefs: LocalRefs = {}
const schemaRefs: Set<string> = new Set()

@@ -108,7 +109,9 @@ export function getSchemaRefs(this: Ajv, schema: AnySchema, baseId: string): Loc
baseIds[jsonPtr] = baseId

function addRef(this: Ajv, ref: string): string {
ref = normalizeId(baseId ? URI.resolve(baseId, ref) : ref)
// eslint-disable-next-line @typescript-eslint/unbound-method
const _resolve = this.opts.uriResolver.resolve
ref = normalizeId(baseId ? _resolve(baseId, ref) : ref)
if (schemaRefs.has(ref)) throw ambiguos(ref)
schemaRefs.add(ref)
let schOrRef = this.refs[ref]
2 changes: 1 addition & 1 deletion lib/compile/validate/index.ts
Original file line number Diff line number Diff line change
@@ -177,7 +177,7 @@ function checkNoDefault(it: SchemaObjCxt): void {

function updateContext(it: SchemaObjCxt): void {
const schId = it.schema[it.opts.schemaId]
if (schId) it.baseId = resolveUrl(it.baseId, schId)
if (schId) it.baseId = resolveUrl(it.opts.uriResolver, it.baseId, schId)
}

function checkAsyncSchema(it: SchemaObjCxt): void {
10 changes: 8 additions & 2 deletions lib/core.ts
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ import type {
Format,
AddedFormat,
RegExpEngine,
UriResolver,
} from "./types"
import type {JSONSchemaType} from "./types/json-schema"
import type {JTDSchemaType, SomeJTDSchemaType, JTDDataType} from "./types/jtd-schema"
@@ -60,9 +61,10 @@ import {Code, ValueScope} from "./compile/codegen"
import {normalizeId, getSchemaRefs} from "./compile/resolve"
import {getJSONTypes} from "./compile/validate/dataType"
import {eachItem} from "./compile/util"

import * as $dataRefSchema from "./refs/data.json"

import DefaultUriResolver from "./runtime/uri"

const defaultRegExp: RegExpEngine = (str, flags) => new RegExp(str, flags)
defaultRegExp.code = "new RegExp"

@@ -136,6 +138,7 @@ export interface CurrentOptions {
int32range?: boolean // JTD only
messages?: boolean
code?: CodeOptions // NEW
uriResolver?: UriResolver
}

export interface CodeOptions {
@@ -226,7 +229,8 @@ type RequiredInstanceOptions = {
| "validateSchema"
| "validateFormats"
| "int32range"
| "unicodeRegExp"]: NonNullable<Options[K]>
| "unicodeRegExp"
| "uriResolver"]: NonNullable<Options[K]>
} & {code: InstanceCodeOptions}

export type InstanceOptions = Options & RequiredInstanceOptions
@@ -239,6 +243,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions {
const _optz = o.code?.optimize
const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
const regExp = o.code?.regExp ?? defaultRegExp
const uriResolver = o.uriResolver ?? DefaultUriResolver
return {
strictSchema: o.strictSchema ?? s ?? true,
strictNumbers: o.strictNumbers ?? s ?? true,
@@ -257,6 +262,7 @@ function requiredOptions(o: Options): RequiredInstanceOptions {
validateFormats: o.validateFormats ?? true,
unicodeRegExp: o.unicodeRegExp ?? true,
int32range: o.int32range ?? true,
uriResolver: uriResolver,
}
}

6 changes: 6 additions & 0 deletions lib/runtime/uri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as uri from "uri-js"

type URI = typeof uri & {code: string}
;(uri as URI).code = 'require("ajv/dist/runtime/uri").default'

export default uri as URI
7 changes: 7 additions & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as URI from "uri-js"
import type {CodeGen, Code, Name, ScopeValueSets, ValueScopeName} from "../compile/codegen"
import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile"
import type {JSONType} from "../compile/rules"
@@ -231,3 +232,9 @@ export interface RegExpEngine {
export interface RegExpLike {
test: (s: string) => boolean
}

export interface UriResolver {
parse(uri: string): URI.URIComponents
resolve(base: string, path: string): string
serialize(component: URI.URIComponents): string
}
2 changes: 1 addition & 1 deletion lib/vocabularies/core/ref.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ const def: CodeKeywordDefinition = {
const {root} = env
if (($ref === "#" || $ref === "#/") && baseId === root.baseId) return callRootRef()
const schOrEnv = resolveRef.call(self, root, baseId, $ref)
if (schOrEnv === undefined) throw new MissingRefError(baseId, $ref)
if (schOrEnv === undefined) throw new MissingRefError(it.opts.uriResolver, baseId, $ref)
if (schOrEnv instanceof SchemaEnv) return callValidate(schOrEnv)
return inlineRefSchema(schOrEnv)

4 changes: 3 additions & 1 deletion lib/vocabularies/jtd/ref.ts
Original file line number Diff line number Diff line change
@@ -28,7 +28,9 @@ const def: CodeKeywordDefinition = {

function validateJtdRef(): void {
const refSchema = (root.schema as AnySchemaObject).definitions?.[ref]
if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`)
if (!refSchema) {
throw new MissingRefError(it.opts.uriResolver, "", ref, `No definition ${ref}`)
}
if (hasRef(refSchema) || !it.opts.inlineRefs) callValidate(refSchema)
else inlineRefSchema(refSchema)
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "8.9.0",
"version": "8.10.0",
"description": "Another JSON Schema Validator",
"main": "dist/ajv.js",
"types": "dist/ajv.d.ts",
@@ -83,6 +83,7 @@
"dayjs-plugin-utc": "^0.1.2",
"eslint": "^7.8.1",
"eslint-config-prettier": "^7.0.0",
"fast-uri": "^1.0.0",
"glob": "^7.0.0",
"husky": "^7.0.1",
"if-node-version": "^1.0.0",
2 changes: 1 addition & 1 deletion spec/keyword.spec.ts
Original file line number Diff line number Diff line change
@@ -282,7 +282,7 @@ describe("User-defined keywords", () => {
it.baseId.should.equal("#")
const ref = schema.$ref
const validate = _ajv.getSchema(ref)
if (!validate) throw new _Ajv.MissingRefError(it.baseId, ref)
if (!validate) throw new _Ajv.MissingRefError(_ajv.opts.uriResolver, it.baseId, ref)
return validate.schema
},
metaSchema: {
Loading