From ad0cf01482ee2424a806ec503dc4c8bc49007f03 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 2 Feb 2021 20:13:03 +0000 Subject: [PATCH 01/29] jtd: run tests (skipped) --- .gitmodules | 3 ++ lib/jtd.ts | 61 +++++++++++++++++++++++++++++++++++++++++ scripts/jsontests.js | 17 ++++++++---- spec/ajv_jtd.ts | 6 ++++ spec/json-typedef-spec | 1 + spec/jtd-schema.spec.ts | 34 +++++++++++++++++++++++ 6 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 lib/jtd.ts create mode 100644 spec/ajv_jtd.ts create mode 160000 spec/json-typedef-spec create mode 100644 spec/jtd-schema.spec.ts diff --git a/.gitmodules b/.gitmodules index 7093cce91..4dbf7adfc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "spec/JSON-Schema-Test-Suite"] path = spec/JSON-Schema-Test-Suite url = https://github.com/json-schema/JSON-Schema-Test-Suite.git +[submodule "spec/json-typedef-spec"] + path = spec/json-typedef-spec + url = git@github.com:jsontypedef/json-typedef-spec.git diff --git a/lib/jtd.ts b/lib/jtd.ts new file mode 100644 index 000000000..6f3fc3fb7 --- /dev/null +++ b/lib/jtd.ts @@ -0,0 +1,61 @@ +export { + Format, + FormatDefinition, + AsyncFormatDefinition, + KeywordDefinition, + KeywordErrorDefinition, + CodeKeywordDefinition, + MacroKeywordDefinition, + FuncKeywordDefinition, + Vocabulary, + Schema, + SchemaObject, + AnySchemaObject, + AsyncSchema, + AnySchema, + ValidateFunction, + AsyncValidateFunction, + ErrorObject, + ErrorNoParams, +} from "./types" + +export {Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions} from "./core" +export {SchemaCxt, SchemaObjCxt} from "./compile" +import KeywordCxt from "./compile/context" +export {KeywordCxt} +// export {DefinedError} from "./vocabularies/errors" +// export {JSONType} from "./compile/rules" +// export {JSONSchemaType} from "./types/json-schema" +export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" + +// import type {AnySchemaObject} from "./types" +import AjvCore from "./core" +// import draft7Vocabularies from "./vocabularies/draft7" +// import draft7MetaSchema = require("./refs/json-schema-draft-07.json") + +// const META_SUPPORT_DATA = ["/properties"] + +// const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" + +export default class Ajv extends AjvCore { + _addVocabularies(): void { + super._addVocabularies() + // draft7Vocabularies.forEach((v) => this.addVocabulary(v)) + } + + // _addDefaultMetaSchema(): void { + // super._addDefaultMetaSchema() + // const {$data, meta} = this.opts + // if (!meta) return + // const metaSchema = $data + // ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) + // : draft7MetaSchema + // this.addMetaSchema(metaSchema, META_SCHEMA_ID, false) + // this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID + // } + + // defaultMeta(): string | AnySchemaObject | undefined { + // return (this.opts.defaultMeta = + // super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined)) + // } +} diff --git a/scripts/jsontests.js b/scripts/jsontests.js index 88423d81b..0102ae1d1 100644 --- a/scripts/jsontests.js +++ b/scripts/jsontests.js @@ -1,6 +1,6 @@ "use strict" -const testSuitePaths = { +const testSuiteConfig = { draft6: "spec/JSON-Schema-Test-Suite/tests/draft6/", draft7: "spec/JSON-Schema-Test-Suite/tests/draft7/", draft2019: "spec/JSON-Schema-Test-Suite/tests/draft2019-09/", @@ -8,13 +8,17 @@ const testSuitePaths = { security: "spec/security/", extras: "spec/extras/", async: "spec/async/", + jtd: {path: "spec/json-typedef-spec/tests/", export: "object"}, } const glob = require("glob") const fs = require("fs") -for (const suite in testSuitePaths) { - const p = testSuitePaths[suite] +for (const suite in testSuiteConfig) { + const cfg = testSuiteConfig[suite] + const isStr = typeof cfg == "string" + const p = isStr ? cfg : cfg.path + const exp = isStr ? "array" : cfg.export const files = glob.sync(`${p}{**/,}*.json`) if (files.length === 0) { console.error(`Missing folder ${p}\nTry: git submodule update --init\n`) @@ -24,8 +28,11 @@ for (const suite in testSuitePaths) { .map((f) => { const name = f.replace(p, "").replace(/\.json$/, "") const testPath = f.replace(/^spec/, "..") - return `\n {name: "${name}", test: require("${testPath}")},` + return exp === "array" + ? `\n {name: "${name}", test: require("${testPath}")},` + : `\n ${name}: require("${testPath}"),` }) .reduce((list, f) => list + f) - fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = [${code}\n]\n`) + const exportCode = exp === "array" ? `[${code}\n]` : `{${code}\n}` + fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = ${exportCode}\n`) } diff --git a/spec/ajv_jtd.ts b/spec/ajv_jtd.ts new file mode 100644 index 000000000..aa3fa0253 --- /dev/null +++ b/spec/ajv_jtd.ts @@ -0,0 +1,6 @@ +import type AjvJTD from "../dist/jtd" + +const m = typeof window == "object" ? (window as any).ajvJTD : require("" + "../dist/jtd") +const AjvClass: typeof AjvJTD = m.default + +export default AjvClass diff --git a/spec/json-typedef-spec b/spec/json-typedef-spec new file mode 160000 index 000000000..71ca27584 --- /dev/null +++ b/spec/json-typedef-spec @@ -0,0 +1 @@ +Subproject commit 71ca275847318717c36f5a2322a8061070fe185d diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts new file mode 100644 index 000000000..c29460f02 --- /dev/null +++ b/spec/jtd-schema.spec.ts @@ -0,0 +1,34 @@ +import type Ajv from ".." +import type {SchemaObject} from ".." +import _AjvJTD from "./ajv_jtd" +import {validation} from "./_json/jtd" +import assert = require("assert") + +interface TestCase { + schema: SchemaObject + instance: unknown + errors: TestCaseError[] +} + +interface TestCaseError { + instancePath: string[] + schemaPath: string[] +} + +describe.skip("JTD validation", () => { + let ajv: Ajv + + before(() => { + ajv = new _AjvJTD() + }) + + for (const testName in validation) { + const {schema, instance, errors} = validation[testName] as TestCase + const valid = errors.length === 0 + describe(testName, () => { + it(`should be ${valid ? "valid" : "invalid"}`, () => { + assert.strictEqual(ajv.validate(schema, instance), valid) + }) + }) + } +}) From e62eb8217ef87c77854913008ac08ebecaed67e2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 3 Feb 2021 18:53:26 +0000 Subject: [PATCH 02/29] JTD keywords: type, enum, elements --- .prettierignore | 1 + lib/compile/rules.ts | 4 +- lib/compile/valid_date.ts | 45 +++++++++++++++++++ lib/compile/validate/index.ts | 1 + lib/compile/validate/iterate.ts | 2 +- lib/core.ts | 1 + lib/jtd.ts | 13 ++++-- lib/vocabularies/applicator/items.ts | 30 +++---------- lib/vocabularies/code.ts | 23 +++++++++- lib/vocabularies/core/index.ts | 1 + lib/vocabularies/jtd/elements.ts | 27 ++++++++++++ lib/vocabularies/jtd/enum.ts | 39 +++++++++++++++++ lib/vocabularies/jtd/index.ts | 8 ++++ lib/vocabularies/jtd/type.ts | 64 ++++++++++++++++++++++++++++ lib/vocabularies/validation/index.ts | 1 + spec/jtd-schema.spec.ts | 22 +++++++--- spec/keyword.spec.ts | 5 +-- 17 files changed, 248 insertions(+), 39 deletions(-) create mode 100644 lib/compile/valid_date.ts create mode 100644 lib/vocabularies/jtd/elements.ts create mode 100644 lib/vocabularies/jtd/enum.ts create mode 100644 lib/vocabularies/jtd/index.ts create mode 100644 lib/vocabularies/jtd/type.ts diff --git a/.prettierignore b/.prettierignore index c61c4f88f..3f5b3ccba 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ spec/JSON-Schema-Test-Suite +spec/json-typedef-spec .browser coverage dist diff --git a/lib/compile/rules.ts b/lib/compile/rules.ts index 7d50819a6..ea65074f9 100644 --- a/lib/compile/rules.ts +++ b/lib/compile/rules.ts @@ -44,7 +44,7 @@ export function getRules(): ValidationRules { types: {...groups, integer: true, boolean: true, null: true}, rules: [{rules: []}, groups.number, groups.string, groups.array, groups.object], post: {rules: []}, - all: {type: true, $comment: true}, - keywords: {type: true, $comment: true}, + all: {}, + keywords: {}, } } diff --git a/lib/compile/valid_date.ts b/lib/compile/valid_date.ts new file mode 100644 index 000000000..bb8ae8ed9 --- /dev/null +++ b/lib/compile/valid_date.ts @@ -0,0 +1,45 @@ +const DATE_TIME_SEPARATOR = /t|\s/i +const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ +const TIME = /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|([+-]\d\d)(?::?(\d\d))?)$/i +const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +export default function validDate(str: string): boolean { + // http://tools.ietf.org/html/rfc3339#section-5.6 + const dateTime: string[] = str.split(DATE_TIME_SEPARATOR) + return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1]) +} + +function isLeapYear(year: number): boolean { + // https://tools.ietf.org/html/rfc3339#appendix-C + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) +} + +function date(str: string): boolean { + // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 + const matches: string[] | null = DATE.exec(str) + if (!matches) return false + const year: number = +matches[1] + const month: number = +matches[2] + const day: number = +matches[3] + return ( + month >= 1 && + month <= 12 && + day >= 1 && + day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]) + ) +} + +function time(str: string): boolean { + const matches: string[] | null = TIME.exec(str) + if (!matches) return false + + const hour: number = +matches[1] + const minute: number = +matches[2] + const second: number = +matches[3] + const tzHour: number = +(matches[4] || 0) + const tzMin: number = +(matches[5] || 0) + return ( + (hour <= 23 && minute <= 59 && second <= 59) || + (hour - tzHour === 23 && minute - tzMin === 59 && second === 60) + ) +} diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 107a7f7e7..474d09043 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -131,6 +131,7 @@ function checkKeywords(it: SchemaObjCxt): void { } function typeAndKeywords(it: SchemaObjCxt, errsCount?: Name): void { + if (it.opts.jtd) return schemaKeywords(it, [], false, errsCount) const types = getSchemaTypes(it.schema) const checkedTypes = coerceAndCheckDataType(it, types) schemaKeywords(it, types, !checkedTypes, errsCount) diff --git a/lib/compile/validate/iterate.ts b/lib/compile/validate/iterate.ts index 24b1d5282..c68f98d2e 100644 --- a/lib/compile/validate/iterate.ts +++ b/lib/compile/validate/iterate.ts @@ -21,7 +21,7 @@ export function schemaKeywords( gen.block(() => keywordCode(it, "$ref", (RULES.all.$ref as Rule).definition)) // TODO typecast return } - checkStrictTypes(it, types) + if (!opts.jtd) checkStrictTypes(it, types) gen.block(() => { for (const group of RULES.rules) groupKeywords(group) groupKeywords(RULES.post) diff --git a/lib/core.ts b/lib/core.ts index a5eed91ce..7ff14fa09 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -105,6 +105,7 @@ interface CurrentOptions { next?: boolean // NEW unevaluated?: boolean // NEW dynamicRef?: boolean // NEW + jtd?: boolean // NEW meta?: SchemaObject | boolean defaultMeta?: string | AnySchemaObject validateSchema?: boolean | "log" diff --git a/lib/jtd.ts b/lib/jtd.ts index 6f3fc3fb7..a84c392b1 100644 --- a/lib/jtd.ts +++ b/lib/jtd.ts @@ -29,8 +29,8 @@ export {KeywordCxt} export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" // import type {AnySchemaObject} from "./types" -import AjvCore from "./core" -// import draft7Vocabularies from "./vocabularies/draft7" +import AjvCore, {Options} from "./core" +import jtdVocabulary from "./vocabularies/jtd" // import draft7MetaSchema = require("./refs/json-schema-draft-07.json") // const META_SUPPORT_DATA = ["/properties"] @@ -38,9 +38,16 @@ import AjvCore from "./core" // const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" export default class Ajv extends AjvCore { + constructor(opts: Options = {}) { + super({ + ...opts, + jtd: true, + }) + } + _addVocabularies(): void { super._addVocabularies() - // draft7Vocabularies.forEach((v) => this.addVocabulary(v)) + this.addVocabulary(jtdVocabulary) } // _addDefaultMetaSchema(): void { diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 20809149f..29553a16f 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -1,9 +1,9 @@ import type {CodeKeywordDefinition, AnySchema} from "../../types" import type KeywordCxt from "../../compile/context" -import {_, not} from "../../compile/codegen" -import {Type} from "../../compile/subschema" +import {_} from "../../compile/codegen" import {alwaysValidSchema, mergeEvaluated} from "../../compile/util" import {checkStrictMode} from "../../compile/validate" +import {validateArray} from "../code" const def: CodeKeywordDefinition = { keyword: "items", @@ -11,8 +11,7 @@ const def: CodeKeywordDefinition = { schemaType: ["object", "array", "boolean"], before: "uniqueItems", code(cxt: KeywordCxt) { - const {gen, schema, parentSchema, data, it} = cxt - const len = gen.const("len", _`${data}.length`) + const {gen, schema, it} = cxt if (Array.isArray(schema)) { if (it.opts.unevaluated && schema.length && it.items !== true) { it.items = mergeEvaluated.items(gen, schema.length, it.items) @@ -20,15 +19,17 @@ const def: CodeKeywordDefinition = { validateTuple(schema) } else { it.items = true - if (!alwaysValidSchema(it, schema)) validateArray() + cxt.ok(validateArray(cxt)) } function validateTuple(schArr: AnySchema[]): void { - if (it.opts.strictTuples && !fullTupleSchema(schema.length, parentSchema)) { + const {parentSchema, data} = cxt + if (it.opts.strictTuples && !fullTupleSchema(schArr.length, parentSchema)) { const msg = `"items" is ${schArr.length}-tuple, but minItems or maxItems/additionalItems are not specified or different` checkStrictMode(it, msg, it.opts.strictTuples) } const valid = gen.name("valid") + const len = gen.const("len", _`${data}.length`) schArr.forEach((sch: AnySchema, i: number) => { if (alwaysValidSchema(it, sch)) return gen.if(_`${len} > ${i}`, () => @@ -45,23 +46,6 @@ const def: CodeKeywordDefinition = { cxt.ok(valid) }) } - - function validateArray(): void { - const valid = gen.name("valid") - gen.forRange("i", 0, len, (i) => { - cxt.subschema( - { - keyword: "items", - dataProp: i, - dataPropType: Type.Num, - strictSchema: it.strictSchema, - }, - valid - ) - if (!it.allErrors) gen.if(not(valid), () => gen.break()) - }) - cxt.ok(valid) - } }, } diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index 3110e5c76..3c51fb0a9 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -1,8 +1,9 @@ import type {AnySchema, SchemaMap} from "../types" import type {SchemaCxt} from "../compile" import type KeywordCxt from "../compile/context" -import {CodeGen, _, or, nil, strConcat, getProperty, Code, Name} from "../compile/codegen" +import {CodeGen, _, or, not, nil, strConcat, getProperty, Code, Name} from "../compile/codegen" import {alwaysValidSchema} from "../compile/util" +import {Type} from "../compile/subschema" import N from "../compile/names" export function checkReportMissingProp(cxt: KeywordCxt, prop: string): void { @@ -83,3 +84,23 @@ export function usePattern(gen: CodeGen, pattern: string): Name { code: _`new RegExp(${pattern}, "u")`, }) } + +export function validateArray(cxt: KeywordCxt): Name | boolean { + const {gen, data, schema, keyword, it} = cxt + if (alwaysValidSchema(it, schema)) return true + const valid = gen.name("valid") + const len = gen.const("len", _`${data}.length`) + gen.forRange("i", 0, len, (i) => { + cxt.subschema( + { + keyword, + dataProp: i, + dataPropType: Type.Num, + strictSchema: it.strictSchema, + }, + valid + ) + if (!it.allErrors) gen.if(not(valid), () => gen.break()) + }) + return valid +} diff --git a/lib/vocabularies/core/index.ts b/lib/vocabularies/core/index.ts index 28e86a76a..e63e2895d 100644 --- a/lib/vocabularies/core/index.ts +++ b/lib/vocabularies/core/index.ts @@ -7,6 +7,7 @@ const core: Vocabulary = [ "$id", "$defs", "$vocabulary", + {keyword: "$comment"}, "definitions", idKeyword, refKeyword, diff --git a/lib/vocabularies/jtd/elements.ts b/lib/vocabularies/jtd/elements.ts new file mode 100644 index 000000000..e5570fa8e --- /dev/null +++ b/lib/vocabularies/jtd/elements.ts @@ -0,0 +1,27 @@ +import type {CodeKeywordDefinition} from "../../types" +import {validateArray} from "../code" +import {_, not, and, Code} from "../../compile/codegen" + +const def: CodeKeywordDefinition = { + keyword: "elements", + schemaType: "object", + code(cxt) { + const {gen, data, parentSchema} = cxt + const valid = gen.name("valid") + let cond: Code = _`Array.isArray(${data})` + if (parentSchema.nullable) { + gen.let(valid, _`${data} === null`) + cond = and(not(valid), cond) + } else { + gen.let(valid) + } + gen.if(cond, () => + gen + .assign(valid, _`${data}.length === 0`) + .if(not(valid), () => gen.assign(valid, validateArray(cxt))) + ) + cxt.pass(valid) + }, +} + +export default def diff --git a/lib/vocabularies/jtd/enum.ts b/lib/vocabularies/jtd/enum.ts new file mode 100644 index 000000000..aa39abbe0 --- /dev/null +++ b/lib/vocabularies/jtd/enum.ts @@ -0,0 +1,39 @@ +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" +import {_, or, not, Code} from "../../compile/codegen" + +const def: CodeKeywordDefinition = { + keyword: "enum", + schemaType: "array", + code(cxt: KeywordCxt) { + const {gen, data, schema, schemaValue, parentSchema, it} = cxt + if (schema.length === 0) throw new Error("enum must have non-empty array") + let valid: Code + if (schema.length >= it.opts.loopEnum) { + if (parentSchema.nullable) { + valid = gen.let("valid", _`${data} === null`) + gen.if(not(valid), loopEnum) + } else { + valid = gen.let("valid", false) + loopEnum() + } + } else { + /* istanbul ignore if */ + if (!Array.isArray(schema)) throw new Error("ajv implementation error") + valid = or(...schema.map((value: string) => _`${data} === ${value}`)) + if (parentSchema.nullable) valid = or(_`${data} === null`, valid) + } + cxt.pass(valid) + + function loopEnum(): void { + gen.forOf("v", schemaValue as Code, (v) => + gen.if(_`${valid} = ${data} === ${v}`, () => gen.break()) + ) + } + }, + metaSchema: { + elements: {type: "string"}, + }, +} + +export default def diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts new file mode 100644 index 000000000..e8a028021 --- /dev/null +++ b/lib/vocabularies/jtd/index.ts @@ -0,0 +1,8 @@ +import type {Vocabulary} from "../../types" +import typeKeyword from "./type" +import enumKeyword from "./enum" +import elements from "./elements" + +const jtdVocabulary: Vocabulary = [typeKeyword, enumKeyword, elements] + +export default jtdVocabulary diff --git a/lib/vocabularies/jtd/type.ts b/lib/vocabularies/jtd/type.ts new file mode 100644 index 000000000..61efc1d8c --- /dev/null +++ b/lib/vocabularies/jtd/type.ts @@ -0,0 +1,64 @@ +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" +import {_, and, Code} from "../../compile/codegen" +import validDate from "../../compile/valid_date" + +type IntType = "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" + +const intRange: {[T in IntType]: [number, number]} = { + int8: [-128, 127], + uint8: [0, 255], + int16: [-32768, 32767], + uint16: [0, 65535], + int32: [-2147483648, 2147483647], + uint32: [0, 4294967295], +} + +const def: CodeKeywordDefinition = { + keyword: "type", + schemaType: "string", + code(cxt: KeywordCxt) { + const {gen, data, schema, parentSchema} = cxt + let cond: Code + switch (schema) { + case "boolean": + case "string": + cond = _`typeof ${data} !== ${schema}` + break + case "timestamp": { + const vd = gen.scopeValue("func", { + ref: validDate, + code: _`require("ajv/dist/compile/valid_date").default`, + }) + cond = _`!(${data} instanceof Date || (typeof ${data} == "string" && ${vd}(${data})))` + break + } + case "float32": + case "float64": + cond = _`typeof ${data} !== "number"` + break + default: { + const [min, max] = intRange[schema as IntType] + cond = _`typeof ${data} == "number" && isFinite(${data}) && ${data} >= ${min} && ${data} <= ${max} && !(${data} % 1)` + } + } + cxt.fail(parentSchema.nullable ? and(cond, _`${data} !== null`) : cond) + }, + metaSchema: { + enum: [ + "boolean", + "timestamp", + "string", + "float32", + "float64", + "int8", + "uint8", + "int16", + "uint16", + "int32", + "uint32", + ], + }, +} + +export default def diff --git a/lib/vocabularies/validation/index.ts b/lib/vocabularies/validation/index.ts index 12b5d6d98..3531b1962 100644 --- a/lib/vocabularies/validation/index.ts +++ b/lib/vocabularies/validation/index.ts @@ -24,6 +24,7 @@ const validation: Vocabulary = [ limitItems, uniqueItems, // any + {keyword: "type", schemaType: ["string", "array"]}, {keyword: "nullable", schemaType: "boolean"}, constKeyword, enumKeyword, diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index c29460f02..cb398727b 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -1,5 +1,5 @@ -import type Ajv from ".." -import type {SchemaObject} from ".." +import type AjvJTD from "../dist/jtd" +import type {SchemaObject} from "../dist/jtd" import _AjvJTD from "./ajv_jtd" import {validation} from "./_json/jtd" import assert = require("assert") @@ -15,20 +15,32 @@ interface TestCaseError { schemaPath: string[] } +const ONLY: RegExp[] = [] + describe.skip("JTD validation", () => { - let ajv: Ajv + let ajv: AjvJTD before(() => { - ajv = new _AjvJTD() + ajv = new _AjvJTD({strict: false}) }) for (const testName in validation) { const {schema, instance, errors} = validation[testName] as TestCase const valid = errors.length === 0 - describe(testName, () => { + describeOnly(testName, () => { it(`should be ${valid ? "valid" : "invalid"}`, () => { + // console.log(schema) + // console.log(ajv.compile(schema).toString()) assert.strictEqual(ajv.validate(schema, instance), valid) }) }) } }) + +function describeOnly(name: string, func: () => void) { + if (ONLY.length > 0 && ONLY.some((p) => p.test(name))) { + describe.only(name, func) + } else { + describe(name, func) + } +} diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index 37524c198..751c43a6e 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -1154,10 +1154,7 @@ describe("User-defined keywords", () => { describe("getKeyword", () => { // TODO update this test - it("should return boolean for reserved and unknown keywords", () => { - ajv.getKeyword("type").should.equal(true) - // ajv.getKeyword("properties").should.equal(true) - // ajv.getKeyword("additionalProperties").should.equal(true) + it("should return false for unknown keywords", () => { ajv.getKeyword("unknown").should.equal(false) }) From fb78ba37848d006db2fbc869de37d0b31a24e81a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 3 Feb 2021 21:33:14 +0000 Subject: [PATCH 03/29] revert script to generate json tests from folders --- scripts/jsontests.js | 17 +++++------------ spec/jtd-schema.spec.ts | 6 +++--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/scripts/jsontests.js b/scripts/jsontests.js index 0102ae1d1..88423d81b 100644 --- a/scripts/jsontests.js +++ b/scripts/jsontests.js @@ -1,6 +1,6 @@ "use strict" -const testSuiteConfig = { +const testSuitePaths = { draft6: "spec/JSON-Schema-Test-Suite/tests/draft6/", draft7: "spec/JSON-Schema-Test-Suite/tests/draft7/", draft2019: "spec/JSON-Schema-Test-Suite/tests/draft2019-09/", @@ -8,17 +8,13 @@ const testSuiteConfig = { security: "spec/security/", extras: "spec/extras/", async: "spec/async/", - jtd: {path: "spec/json-typedef-spec/tests/", export: "object"}, } const glob = require("glob") const fs = require("fs") -for (const suite in testSuiteConfig) { - const cfg = testSuiteConfig[suite] - const isStr = typeof cfg == "string" - const p = isStr ? cfg : cfg.path - const exp = isStr ? "array" : cfg.export +for (const suite in testSuitePaths) { + const p = testSuitePaths[suite] const files = glob.sync(`${p}{**/,}*.json`) if (files.length === 0) { console.error(`Missing folder ${p}\nTry: git submodule update --init\n`) @@ -28,11 +24,8 @@ for (const suite in testSuiteConfig) { .map((f) => { const name = f.replace(p, "").replace(/\.json$/, "") const testPath = f.replace(/^spec/, "..") - return exp === "array" - ? `\n {name: "${name}", test: require("${testPath}")},` - : `\n ${name}: require("${testPath}"),` + return `\n {name: "${name}", test: require("${testPath}")},` }) .reduce((list, f) => list + f) - const exportCode = exp === "array" ? `[${code}\n]` : `{${code}\n}` - fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = ${exportCode}\n`) + fs.writeFileSync(`./spec/_json/${suite}.js`, `module.exports = [${code}\n]\n`) } diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index cb398727b..bcd3bbef8 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -1,7 +1,7 @@ import type AjvJTD from "../dist/jtd" import type {SchemaObject} from "../dist/jtd" import _AjvJTD from "./ajv_jtd" -import {validation} from "./_json/jtd" +import jtdValidationTests = require("./json-typedef-spec/tests/validation.json") import assert = require("assert") interface TestCase { @@ -24,8 +24,8 @@ describe.skip("JTD validation", () => { ajv = new _AjvJTD({strict: false}) }) - for (const testName in validation) { - const {schema, instance, errors} = validation[testName] as TestCase + for (const testName in jtdValidationTests) { + const {schema, instance, errors} = jtdValidationTests[testName] as TestCase const valid = errors.length === 0 describeOnly(testName, () => { it(`should be ${valid ? "valid" : "invalid"}`, () => { From 2e426c29c7ac2da3654684ca7ec1104722112ad4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 3 Feb 2021 21:36:19 +0000 Subject: [PATCH 04/29] update git submodule to use https --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 4dbf7adfc..b397537d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/json-schema/JSON-Schema-Test-Suite.git [submodule "spec/json-typedef-spec"] path = spec/json-typedef-spec - url = git@github.com:jsontypedef/json-typedef-spec.git + url = https://github.com/jsontypedef/json-typedef-spec.git From f0b54874a58e44213d6b902b1f66fc45a7f06186 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 3 Feb 2021 22:03:47 +0000 Subject: [PATCH 05/29] jtd: refactor timestamp --- lib/compile/timestamp.ts | 27 ++++++++++++++++++++++ lib/compile/valid_date.ts | 45 ------------------------------------ lib/vocabularies/jtd/type.ts | 10 ++++---- spec/jtd-schema.spec.ts | 2 +- 4 files changed, 33 insertions(+), 51 deletions(-) create mode 100644 lib/compile/timestamp.ts delete mode 100644 lib/compile/valid_date.ts diff --git a/lib/compile/timestamp.ts b/lib/compile/timestamp.ts new file mode 100644 index 000000000..6543ba56e --- /dev/null +++ b/lib/compile/timestamp.ts @@ -0,0 +1,27 @@ +const DATE_TIME = /^(\d\d\d\d)-(\d\d)-(\d\d)(?:t|\s)(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|([+-]\d\d)(?::?(\d\d))?)$/i +const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +export default function validTimestamp(str: string): boolean { + // http://tools.ietf.org/html/rfc3339#section-5.6 + const matches: string[] | null = DATE_TIME.exec(str) + if (!matches) return false + const y: number = +matches[1] + const m: number = +matches[2] + const d: number = +matches[3] + const hr: number = +matches[4] + const min: number = +matches[5] + const sec: number = +matches[6] + const tzH: number = +(matches[7] || 0) + const tzM: number = +(matches[8] || 0) + return ( + m >= 1 && + m <= 12 && + d >= 1 && + (d <= DAYS[m] || + // leap year: https://tools.ietf.org/html/rfc3339#appendix-C + (m === 2 && d === 29 && (y % 100 === 0 ? y % 400 === 0 : y % 4 === 0))) && + ((hr <= 23 && min <= 59 && sec <= 59) || + // leap second + (hr - tzH === 23 && min - tzM === 59 && sec === 60)) + ) +} diff --git a/lib/compile/valid_date.ts b/lib/compile/valid_date.ts deleted file mode 100644 index bb8ae8ed9..000000000 --- a/lib/compile/valid_date.ts +++ /dev/null @@ -1,45 +0,0 @@ -const DATE_TIME_SEPARATOR = /t|\s/i -const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ -const TIME = /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:z|([+-]\d\d)(?::?(\d\d))?)$/i -const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - -export default function validDate(str: string): boolean { - // http://tools.ietf.org/html/rfc3339#section-5.6 - const dateTime: string[] = str.split(DATE_TIME_SEPARATOR) - return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1]) -} - -function isLeapYear(year: number): boolean { - // https://tools.ietf.org/html/rfc3339#appendix-C - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) -} - -function date(str: string): boolean { - // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 - const matches: string[] | null = DATE.exec(str) - if (!matches) return false - const year: number = +matches[1] - const month: number = +matches[2] - const day: number = +matches[3] - return ( - month >= 1 && - month <= 12 && - day >= 1 && - day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]) - ) -} - -function time(str: string): boolean { - const matches: string[] | null = TIME.exec(str) - if (!matches) return false - - const hour: number = +matches[1] - const minute: number = +matches[2] - const second: number = +matches[3] - const tzHour: number = +(matches[4] || 0) - const tzMin: number = +(matches[5] || 0) - return ( - (hour <= 23 && minute <= 59 && second <= 59) || - (hour - tzHour === 23 && minute - tzMin === 59 && second === 60) - ) -} diff --git a/lib/vocabularies/jtd/type.ts b/lib/vocabularies/jtd/type.ts index 61efc1d8c..ffbde7c21 100644 --- a/lib/vocabularies/jtd/type.ts +++ b/lib/vocabularies/jtd/type.ts @@ -1,7 +1,7 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, and, Code} from "../../compile/codegen" -import validDate from "../../compile/valid_date" +import validTimestamp from "../../compile/timestamp" type IntType = "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" @@ -26,11 +26,11 @@ const def: CodeKeywordDefinition = { cond = _`typeof ${data} !== ${schema}` break case "timestamp": { - const vd = gen.scopeValue("func", { - ref: validDate, - code: _`require("ajv/dist/compile/valid_date").default`, + const vts = gen.scopeValue("func", { + ref: validTimestamp, + code: _`require("ajv/dist/compile/timestamp").default`, }) - cond = _`!(${data} instanceof Date || (typeof ${data} == "string" && ${vd}(${data})))` + cond = _`!(${data} instanceof Date || (typeof ${data} == "string" && ${vts}(${data})))` break } case "float32": diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index bcd3bbef8..8bef66cef 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -15,7 +15,7 @@ interface TestCaseError { schemaPath: string[] } -const ONLY: RegExp[] = [] +const ONLY: RegExp[] = [/timestamp/] describe.skip("JTD validation", () => { let ajv: AjvJTD From 8393e8fd54c489330dba784ef76e1d2a195f72f2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 3 Feb 2021 22:19:38 +0000 Subject: [PATCH 06/29] fix JTD type keyword, enable JTD tests --- lib/vocabularies/jtd/type.ts | 10 +++++----- spec/jtd-schema.spec.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/vocabularies/jtd/type.ts b/lib/vocabularies/jtd/type.ts index ffbde7c21..fbd580667 100644 --- a/lib/vocabularies/jtd/type.ts +++ b/lib/vocabularies/jtd/type.ts @@ -1,6 +1,6 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" -import {_, and, Code} from "../../compile/codegen" +import {_, or, Code} from "../../compile/codegen" import validTimestamp from "../../compile/timestamp" type IntType = "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" @@ -23,26 +23,26 @@ const def: CodeKeywordDefinition = { switch (schema) { case "boolean": case "string": - cond = _`typeof ${data} !== ${schema}` + cond = _`typeof ${data} == ${schema}` break case "timestamp": { const vts = gen.scopeValue("func", { ref: validTimestamp, code: _`require("ajv/dist/compile/timestamp").default`, }) - cond = _`!(${data} instanceof Date || (typeof ${data} == "string" && ${vts}(${data})))` + cond = _`${data} instanceof Date || (typeof ${data} == "string" && ${vts}(${data}))` break } case "float32": case "float64": - cond = _`typeof ${data} !== "number"` + cond = _`typeof ${data} == "number"` break default: { const [min, max] = intRange[schema as IntType] cond = _`typeof ${data} == "number" && isFinite(${data}) && ${data} >= ${min} && ${data} <= ${max} && !(${data} % 1)` } } - cxt.fail(parentSchema.nullable ? and(cond, _`${data} !== null`) : cond) + cxt.pass(parentSchema.nullable ? or(_`${data} === null`, cond) : cond) }, metaSchema: { enum: [ diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 8bef66cef..d4159d310 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -15,13 +15,13 @@ interface TestCaseError { schemaPath: string[] } -const ONLY: RegExp[] = [/timestamp/] +const ONLY: RegExp[] = ["type", "enum", "elements"].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) -describe.skip("JTD validation", () => { +describe("JTD validation", () => { let ajv: AjvJTD before(() => { - ajv = new _AjvJTD({strict: false}) + ajv = new _AjvJTD({strict: false, logger: false}) }) for (const testName in jtdValidationTests) { @@ -39,8 +39,8 @@ describe.skip("JTD validation", () => { function describeOnly(name: string, func: () => void) { if (ONLY.length > 0 && ONLY.some((p) => p.test(name))) { - describe.only(name, func) - } else { describe(name, func) + } else { + describe.skip(name, func) } } From c76b70a5e389dff1842c003f18b3a1c4b9738f02 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 4 Feb 2021 22:34:22 +0000 Subject: [PATCH 07/29] JTD: properties and optionalProperties keywords --- lib/vocabularies/jtd/elements.ts | 2 +- lib/vocabularies/jtd/index.ts | 10 +- lib/vocabularies/jtd/optionalProperties.ts | 13 +++ lib/vocabularies/jtd/properties.ts | 105 +++++++++++++++++++++ spec/jtd-schema.spec.ts | 4 +- 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 lib/vocabularies/jtd/optionalProperties.ts create mode 100644 lib/vocabularies/jtd/properties.ts diff --git a/lib/vocabularies/jtd/elements.ts b/lib/vocabularies/jtd/elements.ts index e5570fa8e..18c00abd8 100644 --- a/lib/vocabularies/jtd/elements.ts +++ b/lib/vocabularies/jtd/elements.ts @@ -13,7 +13,7 @@ const def: CodeKeywordDefinition = { gen.let(valid, _`${data} === null`) cond = and(not(valid), cond) } else { - gen.let(valid) + gen.let(valid, false) } gen.if(cond, () => gen diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index e8a028021..f63796cd6 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -2,7 +2,15 @@ import type {Vocabulary} from "../../types" import typeKeyword from "./type" import enumKeyword from "./enum" import elements from "./elements" +import properties from "./properties" +import optionalProperties from "./optionalProperties" -const jtdVocabulary: Vocabulary = [typeKeyword, enumKeyword, elements] +const jtdVocabulary: Vocabulary = [ + typeKeyword, + enumKeyword, + elements, + properties, + optionalProperties, +] export default jtdVocabulary diff --git a/lib/vocabularies/jtd/optionalProperties.ts b/lib/vocabularies/jtd/optionalProperties.ts new file mode 100644 index 000000000..6fe1ce013 --- /dev/null +++ b/lib/vocabularies/jtd/optionalProperties.ts @@ -0,0 +1,13 @@ +import type {CodeKeywordDefinition} from "../../types" +import {validateProperties} from "./properties" + +const def: CodeKeywordDefinition = { + keyword: "optionalProperties", + schemaType: "object", + code(cxt) { + if (cxt.parentSchema.properties) return + validateProperties(cxt) + }, +} + +export default def diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts new file mode 100644 index 000000000..574f81079 --- /dev/null +++ b/lib/vocabularies/jtd/properties.ts @@ -0,0 +1,105 @@ +import type {CodeKeywordDefinition} from "../../types" +import {propertyInData, allSchemaProperties} from "../code" +import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util" +import {_, not, and, Code, Name} from "../../compile/codegen" +import {KeywordCxt} from "../../ajv" + +const def: CodeKeywordDefinition = { + keyword: "properties", + schemaType: "object", + code: validateProperties, +} + +export function validateProperties(cxt: KeywordCxt): void { + const {gen, data, parentSchema, it} = cxt + const {additionalProperties} = parentSchema + let cond: Code + const [allProps, properties] = schemaProperties("properties") + const [allOptProps, optProperties] = schemaProperties("optionalProperties") + if (properties.length === 0 && optProperties.length === 0 && additionalProperties) { + return + } + + const valid = gen.name("valid") + if (parentSchema.nullable) { + gen.let(valid, _`${data} === null`) + cond = not(valid) + } else { + gen.let(valid, false) + cond = data + } + + gen.if(_`${cond} && typeof ${data} == "object" && !Array.isArray(${data})`, () => { + gen.assign(valid, true) + gen.block(() => { + validateProps(properties, "properties", true) + validateProps(optProperties, "optionalProperties") + if (!additionalProperties) validateAdditional() + }) + }) + cxt.pass(valid) + + function schemaProperties(keyword: string): [string[], string[]] { + const schema = parentSchema[keyword] + const allPs = schema ? allSchemaProperties(schema) : [] + const ps = allPs.filter((p) => !alwaysValidSchema(it, schema[p])) + return [allPs, ps] + } + + function validateProps(props: string[], keyword: string, required?: boolean): void { + for (const prop of props) { + gen.if( + propertyInData(data, prop, it.opts.ownProperties), + () => gen.assign(valid, and(valid, applyPropertySchema(prop, keyword))), + required ? () => gen.assign(valid, false) : undefined + ) + cxt.ok(valid) + } + } + + function applyPropertySchema(prop: string, keyword: string): Name { + const _valid = gen.name("valid") + cxt.subschema( + { + keyword, + schemaProp: prop, + dataProp: prop, + strictSchema: it.strictSchema, + }, + _valid + ) + return _valid + } + + function validateAdditional(): void { + gen.forIn("key", data, (key: Name) => { + const addProp = isAdditional(key, allProps, "properties") + const addOptProp = isAdditional(key, allOptProps, "optionalProperties") + const extra = + addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp) + gen.if(extra, () => { + if (it.opts.removeAdditional) gen.code(_`delete ${data}[${key}]`) + // cxt.setParams({additionalProperty: key}) + cxt.error() + gen.assign(valid, false) + if (!it.opts.allErrors) gen.break() + }) + }) + } + + function isAdditional(key: Name, props: string[], keyword: string): Code | true { + let additional: Code | boolean + if (props.length > 8) { + // TODO maybe an option instead of hard-coded 8? + const propsSchema = schemaRefOrVal(it, parentSchema[keyword], keyword) + additional = _`!${propsSchema}.hasOwnProperty(${key})` + } else if (props.length) { + additional = and(...props.map((p) => _`${key} !== ${p}`)) + } else { + additional = true + } + return additional + } +} + +export default def diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index d4159d310..dc33f7d7c 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -15,7 +15,9 @@ interface TestCaseError { schemaPath: string[] } -const ONLY: RegExp[] = ["type", "enum", "elements"].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) +const ONLY: RegExp[] = ["type", "enum", "elements", "properties", "optionalProperties"].map( + (s) => new RegExp(`(^|.*\\s)${s}\\s.*-`) +) describe("JTD validation", () => { let ajv: AjvJTD From 4cd10a3ef3757e9a1d70decb29d8f1fce71941f9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 4 Feb 2021 22:38:51 +0000 Subject: [PATCH 08/29] test JTD in allErrors mode --- spec/jtd-schema.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index dc33f7d7c..a7018f212 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -20,10 +20,11 @@ const ONLY: RegExp[] = ["type", "enum", "elements", "properties", "optionalPrope ) describe("JTD validation", () => { - let ajv: AjvJTD + let ajv, ajvAE: AjvJTD before(() => { ajv = new _AjvJTD({strict: false, logger: false}) + ajvAE = new _AjvJTD({allErrors: true, strict: false, logger: false}) }) for (const testName in jtdValidationTests) { @@ -34,6 +35,7 @@ describe("JTD validation", () => { // console.log(schema) // console.log(ajv.compile(schema).toString()) assert.strictEqual(ajv.validate(schema, instance), valid) + assert.strictEqual(ajvAE.validate(schema, instance), valid) }) }) } From 11ee873f8ef7728760cfaf6aa9556f520386584b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 5 Feb 2021 08:41:18 +0000 Subject: [PATCH 09/29] JTD values keyword, refactor array validation --- .../applicator/additionalProperties.ts | 2 +- lib/vocabularies/applicator/items.ts | 1 + lib/vocabularies/code.ts | 41 ++++++++------ lib/vocabularies/jtd/elements.ts | 13 ++--- lib/vocabularies/jtd/index.ts | 2 + lib/vocabularies/jtd/optionalProperties.ts | 3 +- lib/vocabularies/jtd/properties.ts | 2 +- lib/vocabularies/jtd/values.ts | 56 +++++++++++++++++++ spec/jtd-schema.spec.ts | 11 +++- 9 files changed, 102 insertions(+), 29 deletions(-) create mode 100644 lib/vocabularies/jtd/values.ts diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index b4ea18fc6..e8adb0302 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -62,7 +62,7 @@ const def: CodeKeywordDefinition & AddedKeywordDefinition = { if (patProps.length) { definedProp = or(definedProp, ...patProps.map((p) => _`${usePattern(gen, p)}.test(${key})`)) } - return _`!(${definedProp})` + return not(definedProp) } function deleteAdditional(key: Name): void { diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 29553a16f..3be134ac3 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -19,6 +19,7 @@ const def: CodeKeywordDefinition = { validateTuple(schema) } else { it.items = true + if (alwaysValidSchema(it, schema)) return cxt.ok(validateArray(cxt)) } diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index 3c51fb0a9..12c671b1b 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -85,22 +85,31 @@ export function usePattern(gen: CodeGen, pattern: string): Name { }) } -export function validateArray(cxt: KeywordCxt): Name | boolean { - const {gen, data, schema, keyword, it} = cxt - if (alwaysValidSchema(it, schema)) return true +export function validateArray(cxt: KeywordCxt): Name { + const {gen, data, keyword, it} = cxt const valid = gen.name("valid") - const len = gen.const("len", _`${data}.length`) - gen.forRange("i", 0, len, (i) => { - cxt.subschema( - { - keyword, - dataProp: i, - dataPropType: Type.Num, - strictSchema: it.strictSchema, - }, - valid - ) - if (!it.allErrors) gen.if(not(valid), () => gen.break()) - }) + if (it.allErrors) { + const validArr = gen.let("valid", true) + validateItems(() => gen.assign(validArr, false)) + return validArr + } + gen.var(valid, true) + validateItems(() => gen.break()) return valid + + function validateItems(notValid: () => void): void { + const len = gen.const("len", _`${data}.length`) + gen.forRange("i", 0, len, (i) => { + cxt.subschema( + { + keyword, + dataProp: i, + dataPropType: Type.Num, + strictSchema: it.strictSchema, + }, + valid + ) + gen.if(not(valid), notValid) + }) + } } diff --git a/lib/vocabularies/jtd/elements.ts b/lib/vocabularies/jtd/elements.ts index 18c00abd8..4ac2cc0a1 100644 --- a/lib/vocabularies/jtd/elements.ts +++ b/lib/vocabularies/jtd/elements.ts @@ -1,12 +1,15 @@ import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" +import {alwaysValidSchema} from "../../compile/util" import {validateArray} from "../code" import {_, not, and, Code} from "../../compile/codegen" const def: CodeKeywordDefinition = { keyword: "elements", schemaType: "object", - code(cxt) { - const {gen, data, parentSchema} = cxt + code(cxt: KeywordCxt) { + const {gen, data, schema, parentSchema, it} = cxt + if (alwaysValidSchema(it, schema)) return const valid = gen.name("valid") let cond: Code = _`Array.isArray(${data})` if (parentSchema.nullable) { @@ -15,11 +18,7 @@ const def: CodeKeywordDefinition = { } else { gen.let(valid, false) } - gen.if(cond, () => - gen - .assign(valid, _`${data}.length === 0`) - .if(not(valid), () => gen.assign(valid, validateArray(cxt))) - ) + gen.if(cond, () => gen.assign(valid, validateArray(cxt))) cxt.pass(valid) }, } diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index f63796cd6..658742794 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -4,6 +4,7 @@ import enumKeyword from "./enum" import elements from "./elements" import properties from "./properties" import optionalProperties from "./optionalProperties" +import values from "./values" const jtdVocabulary: Vocabulary = [ typeKeyword, @@ -11,6 +12,7 @@ const jtdVocabulary: Vocabulary = [ elements, properties, optionalProperties, + values, ] export default jtdVocabulary diff --git a/lib/vocabularies/jtd/optionalProperties.ts b/lib/vocabularies/jtd/optionalProperties.ts index 6fe1ce013..1efd0438f 100644 --- a/lib/vocabularies/jtd/optionalProperties.ts +++ b/lib/vocabularies/jtd/optionalProperties.ts @@ -1,10 +1,11 @@ import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {validateProperties} from "./properties" const def: CodeKeywordDefinition = { keyword: "optionalProperties", schemaType: "object", - code(cxt) { + code(cxt: KeywordCxt) { if (cxt.parentSchema.properties) return validateProperties(cxt) }, diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 574f81079..313110c92 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -1,8 +1,8 @@ import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" import {propertyInData, allSchemaProperties} from "../code" import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util" import {_, not, and, Code, Name} from "../../compile/codegen" -import {KeywordCxt} from "../../ajv" const def: CodeKeywordDefinition = { keyword: "properties", diff --git a/lib/vocabularies/jtd/values.ts b/lib/vocabularies/jtd/values.ts new file mode 100644 index 000000000..0b3805f4e --- /dev/null +++ b/lib/vocabularies/jtd/values.ts @@ -0,0 +1,56 @@ +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" +import {Type} from "../../compile/subschema" +import {alwaysValidSchema} from "../../compile/util" +import {_, not, and, Code, Name} from "../../compile/codegen" + +const def: CodeKeywordDefinition = { + keyword: "values", + schemaType: "object", + code(cxt: KeywordCxt) { + const {gen, data, schema, parentSchema, it} = cxt + if (alwaysValidSchema(it, schema)) return + const valid = gen.name("valid") + let cond: Code + if (parentSchema.nullable) { + gen.let(valid, _`${data} === null`) + cond = not(valid) + } else { + gen.let(valid, false) + cond = data + } + gen.if(and(cond, _`typeof ${data} == "object" && !Array.isArray(${data})`), () => + gen.assign(valid, validateMap()) + ) + cxt.pass(valid) + + function validateMap(): Name | boolean { + const _valid = gen.name("valid") + if (it.allErrors) { + const validMap = gen.let("valid", true) + validateValues(() => gen.assign(validMap, false)) + return validMap + } + gen.var(_valid, true) + validateValues(() => gen.break()) + return _valid + + function validateValues(notValid: () => void): void { + gen.forIn("key", data, (key) => { + cxt.subschema( + { + keyword: "values", + dataProp: key, + dataPropType: Type.Str, + strictSchema: it.strictSchema, + }, + _valid + ) + gen.if(not(_valid), notValid) + }) + } + } + }, +} + +export default def diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index a7018f212..970ec7c55 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -15,9 +15,14 @@ interface TestCaseError { schemaPath: string[] } -const ONLY: RegExp[] = ["type", "enum", "elements", "properties", "optionalProperties"].map( - (s) => new RegExp(`(^|.*\\s)${s}\\s.*-`) -) +const ONLY: RegExp[] = [ + "type", + "enum", + "elements", + "properties", + "optionalProperties", + "values", +].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) describe("JTD validation", () => { let ajv, ajvAE: AjvJTD From 58c866e2c4c0b6b85d726af25158c396690d8ab5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 5 Feb 2021 22:57:09 +0000 Subject: [PATCH 10/29] discriminator (WIP - no tag exemption), refactor nullable --- lib/vocabularies/jtd/discriminator.ts | 40 +++++++++++++++++++++++++++ lib/vocabularies/jtd/elements.ts | 16 ++++------- lib/vocabularies/jtd/index.ts | 2 ++ lib/vocabularies/jtd/nullable.ts | 18 ++++++++++++ lib/vocabularies/jtd/properties.ts | 21 ++++---------- lib/vocabularies/jtd/values.ts | 19 ++++--------- spec/jtd-schema.spec.ts | 4 +++ 7 files changed, 80 insertions(+), 40 deletions(-) create mode 100644 lib/vocabularies/jtd/discriminator.ts create mode 100644 lib/vocabularies/jtd/nullable.ts diff --git a/lib/vocabularies/jtd/discriminator.ts b/lib/vocabularies/jtd/discriminator.ts new file mode 100644 index 000000000..3ef5286ba --- /dev/null +++ b/lib/vocabularies/jtd/discriminator.ts @@ -0,0 +1,40 @@ +import type {CodeKeywordDefinition} from "../../types" +import type KeywordCxt from "../../compile/context" +import {checkNullableObject} from "./nullable" +import {_, getProperty, Name} from "../../compile/codegen" + +const def: CodeKeywordDefinition = { + keyword: "discriminator", + schemaType: "string", + implements: ["mapping"], + code(cxt: KeywordCxt) { + const {gen, data, schema, parentSchema, it} = cxt + const [valid, cond] = checkNullableObject(cxt, data) + + gen.if(cond, () => { + const tag = gen.const("tag", _`${data}${getProperty(schema)}`) + gen.if(false) + for (const tagValue in parentSchema.mapping) { + gen.elseIf(_`${tag} === ${tagValue}`) + gen.assign(valid, applyTagSchema(tagValue)) + } + gen.endIf() + }) + cxt.pass(valid) + + function applyTagSchema(schemaProp: string): Name { + const _valid = gen.name("valid") + cxt.subschema( + { + keyword: "mapping", + schemaProp, + strictSchema: it.strictSchema, + }, + _valid + ) + return _valid + } + }, +} + +export default def diff --git a/lib/vocabularies/jtd/elements.ts b/lib/vocabularies/jtd/elements.ts index 4ac2cc0a1..dec6bfb11 100644 --- a/lib/vocabularies/jtd/elements.ts +++ b/lib/vocabularies/jtd/elements.ts @@ -2,23 +2,17 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../../compile/util" import {validateArray} from "../code" -import {_, not, and, Code} from "../../compile/codegen" +import {_, and, nil} from "../../compile/codegen" +import {checkNullable} from "./nullable" const def: CodeKeywordDefinition = { keyword: "elements", schemaType: "object", code(cxt: KeywordCxt) { - const {gen, data, schema, parentSchema, it} = cxt + const {gen, data, schema, it} = cxt if (alwaysValidSchema(it, schema)) return - const valid = gen.name("valid") - let cond: Code = _`Array.isArray(${data})` - if (parentSchema.nullable) { - gen.let(valid, _`${data} === null`) - cond = and(not(valid), cond) - } else { - gen.let(valid, false) - } - gen.if(cond, () => gen.assign(valid, validateArray(cxt))) + const [valid, cond] = checkNullable(cxt, nil) + gen.if(and(cond, _`Array.isArray(${data})`), () => gen.assign(valid, validateArray(cxt))) cxt.pass(valid) }, } diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index 658742794..8e2feb08e 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -4,6 +4,7 @@ import enumKeyword from "./enum" import elements from "./elements" import properties from "./properties" import optionalProperties from "./optionalProperties" +import discriminator from "./discriminator" import values from "./values" const jtdVocabulary: Vocabulary = [ @@ -12,6 +13,7 @@ const jtdVocabulary: Vocabulary = [ elements, properties, optionalProperties, + discriminator, values, ] diff --git a/lib/vocabularies/jtd/nullable.ts b/lib/vocabularies/jtd/nullable.ts new file mode 100644 index 000000000..89e4cc111 --- /dev/null +++ b/lib/vocabularies/jtd/nullable.ts @@ -0,0 +1,18 @@ +import type KeywordCxt from "../../compile/context" +import {_, not, Code, Name} from "../../compile/codegen" + +export function checkNullable({gen, data, parentSchema}: KeywordCxt, cond: Code): [Name, Code] { + const valid = gen.name("valid") + if (parentSchema.nullable) { + gen.let(valid, _`${data} === null`) + cond = not(valid) + } else { + gen.let(valid, false) + } + return [valid, cond] +} + +export function checkNullableObject(cxt: KeywordCxt, cond: Code): [Name, Code] { + const [valid, cond_] = checkNullable(cxt, cond) + return [valid, _`${cond_} && typeof ${cxt.data} == "object" && !Array.isArray(${cxt.data})`] +} diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 313110c92..690cf8259 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -2,7 +2,8 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {propertyInData, allSchemaProperties} from "../code" import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util" -import {_, not, and, Code, Name} from "../../compile/codegen" +import {_, and, Code, Name} from "../../compile/codegen" +import {checkNullableObject} from "./nullable" const def: CodeKeywordDefinition = { keyword: "properties", @@ -13,30 +14,20 @@ const def: CodeKeywordDefinition = { export function validateProperties(cxt: KeywordCxt): void { const {gen, data, parentSchema, it} = cxt const {additionalProperties} = parentSchema - let cond: Code const [allProps, properties] = schemaProperties("properties") const [allOptProps, optProperties] = schemaProperties("optionalProperties") if (properties.length === 0 && optProperties.length === 0 && additionalProperties) { return } - const valid = gen.name("valid") - if (parentSchema.nullable) { - gen.let(valid, _`${data} === null`) - cond = not(valid) - } else { - gen.let(valid, false) - cond = data - } - - gen.if(_`${cond} && typeof ${data} == "object" && !Array.isArray(${data})`, () => { - gen.assign(valid, true) - gen.block(() => { + const [valid, cond] = checkNullableObject(cxt, data) + gen.if(cond, () => + gen.assign(valid, true).block(() => { validateProps(properties, "properties", true) validateProps(optProperties, "optionalProperties") if (!additionalProperties) validateAdditional() }) - }) + ) cxt.pass(valid) function schemaProperties(keyword: string): [string[], string[]] { diff --git a/lib/vocabularies/jtd/values.ts b/lib/vocabularies/jtd/values.ts index 0b3805f4e..ba14015e7 100644 --- a/lib/vocabularies/jtd/values.ts +++ b/lib/vocabularies/jtd/values.ts @@ -2,26 +2,17 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {Type} from "../../compile/subschema" import {alwaysValidSchema} from "../../compile/util" -import {_, not, and, Code, Name} from "../../compile/codegen" +import {not, Name} from "../../compile/codegen" +import {checkNullableObject} from "./nullable" const def: CodeKeywordDefinition = { keyword: "values", schemaType: "object", code(cxt: KeywordCxt) { - const {gen, data, schema, parentSchema, it} = cxt + const {gen, data, schema, it} = cxt if (alwaysValidSchema(it, schema)) return - const valid = gen.name("valid") - let cond: Code - if (parentSchema.nullable) { - gen.let(valid, _`${data} === null`) - cond = not(valid) - } else { - gen.let(valid, false) - cond = data - } - gen.if(and(cond, _`typeof ${data} == "object" && !Array.isArray(${data})`), () => - gen.assign(valid, validateMap()) - ) + const [valid, cond] = checkNullableObject(cxt, data) + gen.if(cond, () => gen.assign(valid, validateMap())) cxt.pass(valid) function validateMap(): Name | boolean { diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 970ec7c55..67af7b7c6 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -21,9 +21,12 @@ const ONLY: RegExp[] = [ "elements", "properties", "optionalProperties", + // "discriminator", "values", ].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) +// const ONLY: RegExp[] = [/discriminator schema - ok/] + describe("JTD validation", () => { let ajv, ajvAE: AjvJTD @@ -39,6 +42,7 @@ describe("JTD validation", () => { it(`should be ${valid ? "valid" : "invalid"}`, () => { // console.log(schema) // console.log(ajv.compile(schema).toString()) + // console.log(ajv.validate(schema, instance), ajv.errors) assert.strictEqual(ajv.validate(schema, instance), valid) assert.strictEqual(ajvAE.validate(schema, instance), valid) }) From af00e5fab3d404bfcdb261072ab515e024228466 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 6 Feb 2021 10:19:37 +0000 Subject: [PATCH 11/29] JTD: discriminator tag exemption for additional properties --- lib/compile/index.ts | 1 + lib/compile/subschema.ts | 5 ++++- lib/vocabularies/jtd/discriminator.ts | 15 +++++++++------ lib/vocabularies/jtd/properties.ts | 9 +++++++-- spec/jtd-schema.spec.ts | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 18af11f74..0f6fa2eaf 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -51,6 +51,7 @@ export interface SchemaCxt { // You only need to use it if you have many steps in your keywords and potentially can define multiple errors. props?: EvaluatedProperties | Name // properties evaluated by this schema - used by parent schema or assigned to validation function items?: EvaluatedItems | Name // last item evaluated by this schema - used by parent schema or assigned to validation function + jtdDiscriminator?: string readonly createErrors?: boolean readonly opts: InstanceOptions // Ajv instance option. readonly self: Ajv // current Ajv instance diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 17f8f61d6..7365b144e 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -21,6 +21,7 @@ interface SubschemaContext { dataNames?: Name[] dataPathArr?: (Code | number)[] propertyName?: Name + jtdDiscriminator?: string compositeRule?: true createErrors?: boolean allErrors?: boolean @@ -44,6 +45,7 @@ export type SubschemaArgs = Partial<{ dataTypes: JSONType[] propertyName: Name dataPropType: Type + jtdDiscriminator: string compositeRule: true createErrors: boolean allErrors: boolean @@ -145,11 +147,12 @@ function extendSubschemaData( function extendSubschemaMode( subschema: SubschemaContext, - {compositeRule, createErrors, allErrors, strictSchema}: SubschemaArgs + {jtdDiscriminator, compositeRule, createErrors, allErrors, strictSchema}: SubschemaArgs ): void { if (compositeRule !== undefined) subschema.compositeRule = compositeRule if (createErrors !== undefined) subschema.createErrors = createErrors if (allErrors !== undefined) subschema.allErrors = allErrors + subschema.jtdDiscriminator = jtdDiscriminator // not inherited subschema.strictSchema = strictSchema // not inherited } diff --git a/lib/vocabularies/jtd/discriminator.ts b/lib/vocabularies/jtd/discriminator.ts index 3ef5286ba..98b3bd6e4 100644 --- a/lib/vocabularies/jtd/discriminator.ts +++ b/lib/vocabularies/jtd/discriminator.ts @@ -13,12 +13,14 @@ const def: CodeKeywordDefinition = { gen.if(cond, () => { const tag = gen.const("tag", _`${data}${getProperty(schema)}`) - gen.if(false) - for (const tagValue in parentSchema.mapping) { - gen.elseIf(_`${tag} === ${tagValue}`) - gen.assign(valid, applyTagSchema(tagValue)) - } - gen.endIf() + gen.if(_`typeof ${tag} == "string"`, () => { + gen.if(false) + for (const tagValue in parentSchema.mapping) { + gen.elseIf(_`${tag} === ${tagValue}`) + gen.assign(valid, applyTagSchema(tagValue)) + } + gen.endIf() + }) }) cxt.pass(valid) @@ -29,6 +31,7 @@ const def: CodeKeywordDefinition = { keyword: "mapping", schemaProp, strictSchema: it.strictSchema, + jtdDiscriminator: schema, }, _valid ) diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 690cf8259..e4b48a3fe 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -20,7 +20,10 @@ export function validateProperties(cxt: KeywordCxt): void { return } - const [valid, cond] = checkNullableObject(cxt, data) + const [valid, cond] = + it.jtdDiscriminator === undefined + ? checkNullableObject(cxt, data) + : [gen.let("valid", false), true] gen.if(cond, () => gen.assign(valid, true).block(() => { validateProps(properties, "properties", true) @@ -64,7 +67,9 @@ export function validateProperties(cxt: KeywordCxt): void { function validateAdditional(): void { gen.forIn("key", data, (key: Name) => { - const addProp = isAdditional(key, allProps, "properties") + const _allProps = + it.jtdDiscriminator === undefined ? allProps : [it.jtdDiscriminator].concat(allProps) + const addProp = isAdditional(key, _allProps, "properties") const addOptProp = isAdditional(key, allOptProps, "optionalProperties") const extra = addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 67af7b7c6..db740446e 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -21,7 +21,7 @@ const ONLY: RegExp[] = [ "elements", "properties", "optionalProperties", - // "discriminator", + "discriminator", "values", ].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) From 6ac9f047d6a5819ce9de36b35745ea412c5ca6b0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 6 Feb 2021 20:25:38 +0000 Subject: [PATCH 12/29] JTD: ref form --- lib/compile/context.ts | 4 +- lib/compile/error_classes.ts | 4 +- lib/vocabularies/core/ref.ts | 2 +- lib/vocabularies/jtd/index.ts | 5 +++ lib/vocabularies/jtd/ref.ts | 70 +++++++++++++++++++++++++++++++++++ spec/jtd-schema.spec.ts | 26 +++++++------ 6 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 lib/vocabularies/jtd/ref.ts diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 1d798bfb2..2830b0692 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -25,7 +25,7 @@ export default class KeywordCxt implements KeywordErrorCxt { readonly keyword: string readonly data: Name // Name referencing the current level of the data instance readonly $data?: string | false - readonly schema: any // keyword value in the schema + schema: any // keyword value in the schema readonly schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value readonly schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) readonly schemaType: JSONType[] // allowed type(s) of keyword value in the schema @@ -33,7 +33,7 @@ export default class KeywordCxt implements KeywordErrorCxt { readonly errsCount?: Name // Name reference to the number of validation errors collected before this keyword, // requires option trackErrors in keyword definition params: KeywordCxtParams // object to pass parameters to error messages from keyword code - readonly it: SchemaObjCxt // schema compilation context (schema is guaranted to be an object, not boolean) + readonly it: SchemaObjCxt // schema compilation context (schema is guaranteed to be an object, not boolean) readonly def: AddedKeywordDefinition constructor(it: SchemaObjCxt, def: AddedKeywordDefinition, keyword: string) { diff --git a/lib/compile/error_classes.ts b/lib/compile/error_classes.ts index d24d36b7d..da35f85af 100644 --- a/lib/compile/error_classes.ts +++ b/lib/compile/error_classes.ts @@ -17,8 +17,8 @@ export class MissingRefError extends Error { readonly missingRef: string readonly missingSchema: string - constructor(baseId: string, ref: string) { - super(`can't resolve reference ${ref} from id ${baseId}`) + constructor(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)) } diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index 6c12c2be3..b058607f5 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -10,7 +10,7 @@ import {mergeEvaluated} from "../../compile/util" const def: CodeKeywordDefinition = { keyword: "$ref", schemaType: "string", - code(cxt: KeywordCxt) { + code(cxt: KeywordCxt): void { const {gen, schema, it} = cxt const {baseId, schemaEnv: env, validateName, opts, self} = it // TODO See comment in dynamicRef.ts diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index 8e2feb08e..80c6af5ec 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -1,4 +1,5 @@ import type {Vocabulary} from "../../types" +import refKeyword from "./ref" import typeKeyword from "./type" import enumKeyword from "./enum" import elements from "./elements" @@ -8,6 +9,8 @@ import discriminator from "./discriminator" import values from "./values" const jtdVocabulary: Vocabulary = [ + "definitions", + refKeyword, typeKeyword, enumKeyword, elements, @@ -15,6 +18,8 @@ const jtdVocabulary: Vocabulary = [ optionalProperties, discriminator, values, + {keyword: "nullable", schemaType: "boolean"}, + {keyword: "metadata", schemaType: "object"}, ] export default jtdVocabulary diff --git a/lib/vocabularies/jtd/ref.ts b/lib/vocabularies/jtd/ref.ts new file mode 100644 index 000000000..c1a46778a --- /dev/null +++ b/lib/vocabularies/jtd/ref.ts @@ -0,0 +1,70 @@ +import type {CodeKeywordDefinition, AnySchemaObject} from "../../types" +import type KeywordCxt from "../../compile/context" +import {compileSchema, SchemaEnv} from "../../compile" +import {_, not, nil, stringify} from "../../compile/codegen" +import {MissingRefError} from "../../compile/error_classes" +import N from "../../compile/names" +import {getValidate, callRef} from "../core/ref" + +const def: CodeKeywordDefinition = { + keyword: "ref", + schemaType: "string", + code(cxt: KeywordCxt) { + const {gen, data, schema: ref, parentSchema, it} = cxt + const { + schemaEnv: {root}, + } = it + const valid = gen.name("valid") + if (parentSchema.nullable) { + gen.var(valid, _`${data} === null`) + gen.if(not(valid), validateJtdRef) + } else { + gen.var(valid, false) + validateJtdRef() + } + cxt.pass(valid) + + function validateJtdRef(): void { + const refSchema = (root.schema as AnySchemaObject).definitions?.[ref] + if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`) + if (hasRef(refSchema)) callValidate(refSchema) + else inlineRefSchema(refSchema) + } + + function callValidate(schema: AnySchemaObject): void { + const sch = compileSchema.call(it.self, new SchemaEnv({schema, root})) + const v = getValidate(cxt, sch) + const errsCount = gen.const("_errs", N.errors) + callRef(cxt, v, sch, sch.$async) + gen.assign(valid, _`${errsCount} === ${N.errors}`) + } + + function inlineRefSchema(schema: AnySchemaObject): void { + const schName = gen.scopeValue( + "schema", + it.opts.code.source === true ? {ref: schema, code: stringify(schema)} : {ref: schema} + ) + cxt.subschema( + { + schema, + strictSchema: true, + dataTypes: [], + schemaPath: nil, + topSchemaRef: schName, + errSchemaPath: `#/definitions/${ref}`, + }, + valid + ) + } + + function hasRef(schema: AnySchemaObject): boolean { + for (const key in schema) { + let sch: AnySchemaObject + if (key === "ref" || (typeof (sch = schema[key]) == "object" && hasRef(sch))) return true + } + return false + } + }, +} + +export default def diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index db740446e..8047eeaed 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -15,17 +15,19 @@ interface TestCaseError { schemaPath: string[] } -const ONLY: RegExp[] = [ - "type", - "enum", - "elements", - "properties", - "optionalProperties", - "discriminator", - "values", -].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) - -// const ONLY: RegExp[] = [/discriminator schema - ok/] +// const ONLY: RegExp[] = [ +// "empty", +// "ref", +// "type", +// "enum", +// "elements", +// "properties", +// "optionalProperties", +// "discriminator", +// "values", +// ].map((s) => new RegExp(`(^|.*\\s)${s}\\s.*-`)) + +const ONLY: RegExp[] = [] describe("JTD validation", () => { let ajv, ajvAE: AjvJTD @@ -51,7 +53,7 @@ describe("JTD validation", () => { }) function describeOnly(name: string, func: () => void) { - if (ONLY.length > 0 && ONLY.some((p) => p.test(name))) { + if (ONLY.length === 0 || ONLY.some((p) => p.test(name))) { describe(name, func) } else { describe.skip(name, func) From 894a1330e333c52c6f8bf99edf22857edd6b987b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 6 Feb 2021 20:48:44 +0000 Subject: [PATCH 13/29] test: JTD - 4 ajv instances --- lib/vocabularies/jtd/enum.ts | 3 --- lib/vocabularies/jtd/index.ts | 1 + lib/vocabularies/jtd/type.ts | 30 +++++++++++++++--------------- spec/jtd-schema.spec.ts | 19 +++++++++++-------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/vocabularies/jtd/enum.ts b/lib/vocabularies/jtd/enum.ts index aa39abbe0..df0690e09 100644 --- a/lib/vocabularies/jtd/enum.ts +++ b/lib/vocabularies/jtd/enum.ts @@ -31,9 +31,6 @@ const def: CodeKeywordDefinition = { ) } }, - metaSchema: { - elements: {type: "string"}, - }, } export default def diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index 80c6af5ec..d40736a71 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -18,6 +18,7 @@ const jtdVocabulary: Vocabulary = [ optionalProperties, discriminator, values, + {keyword: "additionalProperties", schemaType: "boolean"}, {keyword: "nullable", schemaType: "boolean"}, {keyword: "metadata", schemaType: "object"}, ] diff --git a/lib/vocabularies/jtd/type.ts b/lib/vocabularies/jtd/type.ts index fbd580667..081b0fef3 100644 --- a/lib/vocabularies/jtd/type.ts +++ b/lib/vocabularies/jtd/type.ts @@ -44,21 +44,21 @@ const def: CodeKeywordDefinition = { } cxt.pass(parentSchema.nullable ? or(_`${data} === null`, cond) : cond) }, - metaSchema: { - enum: [ - "boolean", - "timestamp", - "string", - "float32", - "float64", - "int8", - "uint8", - "int16", - "uint16", - "int32", - "uint32", - ], - }, + // metaSchema: { + // enum: [ + // "boolean", + // "timestamp", + // "string", + // "float32", + // "float64", + // "int8", + // "uint8", + // "int16", + // "uint16", + // "int32", + // "uint32", + // ], + // }, } export default def diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 8047eeaed..42ea14230 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -1,6 +1,7 @@ import type AjvJTD from "../dist/jtd" import type {SchemaObject} from "../dist/jtd" import _AjvJTD from "./ajv_jtd" +import getAjvInstances from "./ajv_instances" import jtdValidationTests = require("./json-typedef-spec/tests/validation.json") import assert = require("assert") @@ -30,11 +31,13 @@ interface TestCaseError { const ONLY: RegExp[] = [] describe("JTD validation", () => { - let ajv, ajvAE: AjvJTD + let ajvs: AjvJTD[] before(() => { - ajv = new _AjvJTD({strict: false, logger: false}) - ajvAE = new _AjvJTD({allErrors: true, strict: false, logger: false}) + ajvs = getAjvInstances(_AjvJTD, { + allErrors: true, + code: {es5: true, lines: true, optimize: false}, + }) }) for (const testName in jtdValidationTests) { @@ -42,11 +45,11 @@ describe("JTD validation", () => { const valid = errors.length === 0 describeOnly(testName, () => { it(`should be ${valid ? "valid" : "invalid"}`, () => { - // console.log(schema) - // console.log(ajv.compile(schema).toString()) - // console.log(ajv.validate(schema, instance), ajv.errors) - assert.strictEqual(ajv.validate(schema, instance), valid) - assert.strictEqual(ajvAE.validate(schema, instance), valid) + ajvs.forEach((ajv) => { + // console.log(ajv.compile(schema).toString()) + // console.log(ajv.validate(schema, instance), ajv.errors) + assert.strictEqual(ajv.validate(schema, instance), valid) + }) }) }) } From c7201841f061960e2554ae2ac8737912f17286ed Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 6 Feb 2021 20:56:07 +0000 Subject: [PATCH 14/29] skipped test for invalid JTD schemas --- spec/jtd-schema.spec.ts | 52 +++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 42ea14230..8f8c45757 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -3,6 +3,7 @@ import type {SchemaObject} from "../dist/jtd" import _AjvJTD from "./ajv_jtd" import getAjvInstances from "./ajv_instances" import jtdValidationTests = require("./json-typedef-spec/tests/validation.json") +import jtdInvalidSchemasTests = require("./json-typedef-spec/tests/invalid_schemas.json") import assert = require("assert") interface TestCase { @@ -30,29 +31,46 @@ interface TestCaseError { const ONLY: RegExp[] = [] -describe("JTD validation", () => { - let ajvs: AjvJTD[] +describe("JSON Type Definition", () => { + describe("validation", () => { + let ajvs: AjvJTD[] - before(() => { - ajvs = getAjvInstances(_AjvJTD, { - allErrors: true, - code: {es5: true, lines: true, optimize: false}, + before(() => { + ajvs = getAjvInstances(_AjvJTD, { + allErrors: true, + code: {es5: true, lines: true, optimize: false}, + }) }) - }) - for (const testName in jtdValidationTests) { - const {schema, instance, errors} = jtdValidationTests[testName] as TestCase - const valid = errors.length === 0 - describeOnly(testName, () => { - it(`should be ${valid ? "valid" : "invalid"}`, () => { - ajvs.forEach((ajv) => { - // console.log(ajv.compile(schema).toString()) - // console.log(ajv.validate(schema, instance), ajv.errors) - assert.strictEqual(ajv.validate(schema, instance), valid) + for (const testName in jtdValidationTests) { + const {schema, instance, errors} = jtdValidationTests[testName] as TestCase + const valid = errors.length === 0 + describeOnly(testName, () => { + it(`should be ${valid ? "valid" : "invalid"}`, () => { + ajvs.forEach((ajv) => { + // console.log(ajv.compile(schema).toString()) + // console.log(ajv.validate(schema, instance), ajv.errors) + assert.strictEqual(ajv.validate(schema, instance), valid) + }) }) }) + } + }) + + describe.skip("invalid schemas", () => { + let ajv: AjvJTD + + before(() => { + ajv = new _AjvJTD() }) - } + + for (const testName in jtdInvalidSchemasTests) { + const schema = jtdInvalidSchemasTests[testName] + it(`${testName} should be invalid schema`, () => { + assert.throws(() => ajv.compile(schema)) + }) + } + }) }) function describeOnly(name: string, func: () => void) { From 103a7402e3514401869127b46a3075486ac4046b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:08:23 +0000 Subject: [PATCH 15/29] test: JTD with standalone code --- spec/jtd-schema.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 8f8c45757..b97172bd0 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -2,6 +2,7 @@ import type AjvJTD from "../dist/jtd" import type {SchemaObject} from "../dist/jtd" import _AjvJTD from "./ajv_jtd" import getAjvInstances from "./ajv_instances" +import {withStandalone} from "./ajv_standalone" import jtdValidationTests = require("./json-typedef-spec/tests/validation.json") import jtdInvalidSchemasTests = require("./json-typedef-spec/tests/invalid_schemas.json") import assert = require("assert") @@ -40,6 +41,7 @@ describe("JSON Type Definition", () => { allErrors: true, code: {es5: true, lines: true, optimize: false}, }) + ajvs.forEach((ajv) => (ajv.opts.code.source = true)) }) for (const testName in jtdValidationTests) { @@ -47,7 +49,7 @@ describe("JSON Type Definition", () => { const valid = errors.length === 0 describeOnly(testName, () => { it(`should be ${valid ? "valid" : "invalid"}`, () => { - ajvs.forEach((ajv) => { + withStandalone(ajvs).forEach((ajv) => { // console.log(ajv.compile(schema).toString()) // console.log(ajv.validate(schema, instance), ajv.errors) assert.strictEqual(ajv.validate(schema, instance), valid) From 83fb45720542a2717ff9696a19fa7d113cc9be82 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:15:12 +0000 Subject: [PATCH 16/29] JTD generate browser bundle --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5813580d7..125391cb1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test-codegen": "nyc cross-env TS_NODE_PROJECT=spec/tsconfig.json mocha -r ts-node/register 'spec/codegen.spec.ts' -R spec", "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", - "bundle": "rm -rf bundle && node ./scripts/bundle.js ajv ajv7 ajv7 && node ./scripts/bundle.js 2019 ajv2019 ajv2019", + "bundle": "rm -rf bundle && node ./scripts/bundle.js ajv ajv7 ajv7 && node ./scripts/bundle.js 2019 ajv2019 ajv2019 && node ./scripts/bundle.js jtd ajvJTD ajvJTD", "build": "rm -rf dist && tsc && cp -r lib/refs dist && rm dist/refs/json-schema-2019-09/index.ts", "json-tests": "rm -rf spec/_json/*.js && node scripts/jsontests", "test-karma": "karma start", From 04752b784f3ad56da5e425bd715f062ddf64699d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 10:53:53 +0000 Subject: [PATCH 17/29] JTD: union keyword, meta-schema, pass invalid_schemas tests --- lib/ajv.ts | 5 +- lib/core.ts | 5 +- lib/jtd.ts | 31 ++--- lib/refs/jtd-schema.ts | 129 ++++++++++++++++++ .../applicator/additionalProperties.ts | 7 +- lib/vocabularies/applicator/anyOf.ts | 39 +----- lib/vocabularies/code.ts | 35 +++++ lib/vocabularies/jtd/enum.ts | 1 + lib/vocabularies/jtd/index.ts | 3 + lib/vocabularies/jtd/properties.ts | 26 +++- lib/vocabularies/jtd/type.ts | 15 -- lib/vocabularies/jtd/union.ts | 15 ++ spec/jtd-schema.spec.ts | 21 ++- 13 files changed, 241 insertions(+), 91 deletions(-) create mode 100644 lib/refs/jtd-schema.ts create mode 100644 lib/vocabularies/jtd/union.ts diff --git a/lib/ajv.ts b/lib/ajv.ts index ce4819182..edcb57204 100644 --- a/lib/ajv.ts +++ b/lib/ajv.ts @@ -45,9 +45,8 @@ export default class Ajv extends AjvCore { _addDefaultMetaSchema(): void { super._addDefaultMetaSchema() - const {$data, meta} = this.opts - if (!meta) return - const metaSchema = $data + if (!this.opts.meta) return + const metaSchema = this.opts.$data ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) : draft7MetaSchema this.addMetaSchema(metaSchema, META_SCHEMA_ID, false) diff --git a/lib/core.ts b/lib/core.ts index 7ff14fa09..421ffb217 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -627,8 +627,9 @@ export default class Ajv { validateSchema = this.opts.validateSchema, addSchema = this.opts.addUsedSchema ): SchemaEnv { - if (typeof schema != "object" && typeof schema != "boolean") { - throw new Error("schema must be object or boolean") + if (typeof schema != "object") { + if (this.opts.jtd) throw new Error("schema must be object") + else if (typeof schema != "boolean") throw new Error("schema must be object or boolean") } let sch = this._cache.get(schema) if (sch !== undefined) return sch diff --git a/lib/jtd.ts b/lib/jtd.ts index a84c392b1..dd15d07aa 100644 --- a/lib/jtd.ts +++ b/lib/jtd.ts @@ -24,18 +24,16 @@ export {SchemaCxt, SchemaObjCxt} from "./compile" import KeywordCxt from "./compile/context" export {KeywordCxt} // export {DefinedError} from "./vocabularies/errors" -// export {JSONType} from "./compile/rules" -// export {JSONSchemaType} from "./types/json-schema" export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" -// import type {AnySchemaObject} from "./types" +import type {AnySchemaObject} from "./types" import AjvCore, {Options} from "./core" import jtdVocabulary from "./vocabularies/jtd" -// import draft7MetaSchema = require("./refs/json-schema-draft-07.json") +import jtdMetaSchema from "./refs/jtd-schema" // const META_SUPPORT_DATA = ["/properties"] -// const META_SCHEMA_ID = "http://json-schema.org/draft-07/schema" +const META_SCHEMA_ID = "JTD-meta-schema" export default class Ajv extends AjvCore { constructor(opts: Options = {}) { @@ -50,19 +48,14 @@ export default class Ajv extends AjvCore { this.addVocabulary(jtdVocabulary) } - // _addDefaultMetaSchema(): void { - // super._addDefaultMetaSchema() - // const {$data, meta} = this.opts - // if (!meta) return - // const metaSchema = $data - // ? this.$dataMetaSchema(draft7MetaSchema, META_SUPPORT_DATA) - // : draft7MetaSchema - // this.addMetaSchema(metaSchema, META_SCHEMA_ID, false) - // this.refs["http://json-schema.org/schema"] = META_SCHEMA_ID - // } + _addDefaultMetaSchema(): void { + super._addDefaultMetaSchema() + if (!this.opts.meta) return + this.addMetaSchema(jtdMetaSchema, META_SCHEMA_ID, false) + } - // defaultMeta(): string | AnySchemaObject | undefined { - // return (this.opts.defaultMeta = - // super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined)) - // } + defaultMeta(): string | AnySchemaObject | undefined { + return (this.opts.defaultMeta = + super.defaultMeta() || (this.getSchema(META_SCHEMA_ID) ? META_SCHEMA_ID : undefined)) + } } diff --git a/lib/refs/jtd-schema.ts b/lib/refs/jtd-schema.ts new file mode 100644 index 000000000..6a79f5c56 --- /dev/null +++ b/lib/refs/jtd-schema.ts @@ -0,0 +1,129 @@ +import {SchemaObject} from "../types" + +type MetaSchema = (root: boolean) => SchemaObject + +const shared: MetaSchema = (root) => { + const sch: SchemaObject = { + nullable: {type: "boolean"}, + metadata: {optionalProperties: {}, additionalProperties: true}, + } + if (root) sch.definitions = {values: {ref: "schema"}} + return sch +} + +const emptyForm: MetaSchema = (root) => ({ + optionalProperties: shared(root), +}) + +const refForm: MetaSchema = (root) => ({ + properties: { + ref: {type: "string"}, + }, + optionalProperties: shared(root), +}) + +const typeForm: MetaSchema = (root) => ({ + properties: { + type: { + enum: [ + "boolean", + "timestamp", + "string", + "float32", + "float64", + "int8", + "uint8", + "int16", + "uint16", + "int32", + "uint32", + ], + }, + }, + optionalProperties: shared(root), +}) + +const enumForm: MetaSchema = (root) => ({ + properties: { + enum: {elements: {type: "string"}}, + }, + optionalProperties: shared(root), +}) + +const elementsForm: MetaSchema = (root) => ({ + properties: { + elements: {ref: "schema"}, + }, + optionalProperties: shared(root), +}) + +const propertiesForm: MetaSchema = (root) => ({ + properties: { + properties: {values: {ref: "schema"}}, + }, + optionalProperties: { + optionalProperties: {values: {ref: "schema"}}, + additionalProperties: {type: "boolean"}, + ...shared(root), + }, +}) + +const optionalPropertiesForm: MetaSchema = (root) => ({ + properties: { + optionalProperties: {values: {ref: "schema"}}, + }, + optionalProperties: { + additionalProperties: {type: "boolean"}, + ...shared(root), + }, +}) + +const discriminatorForm: MetaSchema = (root) => ({ + properties: { + discriminator: {type: "string"}, + mapping: { + values: { + union: [propertiesForm(false), optionalPropertiesForm(false)], + }, + }, + }, + optionalProperties: shared(root), +}) + +const valuesForm: MetaSchema = (root) => ({ + properties: { + values: {ref: "schema"}, + }, + optionalProperties: shared(root), +}) + +const unionForm: MetaSchema = (root) => ({ + properties: { + union: {elements: {ref: "schema"}}, + }, + optionalProperties: shared(root), +}) + +const schema: MetaSchema = (root) => ({ + union: [ + emptyForm, + refForm, + typeForm, + enumForm, + elementsForm, + propertiesForm, + optionalPropertiesForm, + discriminatorForm, + valuesForm, + unionForm, + ].map((s) => s(root)), +}) + +const jtdMetaSchema: SchemaObject = { + definitions: { + schema: schema(false), + }, + ...schema(true), +} + +export default jtdMetaSchema diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index e8adb0302..95f4d0b2f 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -52,8 +52,13 @@ const def: CodeKeywordDefinition & AddedKeywordDefinition = { let definedProp: Code if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? + const hasProp = gen.scopeValue("func", { + // eslint-disable-next-line @typescript-eslint/unbound-method + ref: Object.prototype.hasOwnProperty, + code: _`Object.prototype.hasOwnProperty`, + }) const propsSchema = schemaRefOrVal(it, parentSchema.properties, "properties") - definedProp = _`${propsSchema}.hasOwnProperty(${key})` + definedProp = _`${hasProp}.call(${propsSchema}, ${key})` } else if (props.length) { definedProp = or(...props.map((p) => _`${key} === ${p}`)) } else { diff --git a/lib/vocabularies/applicator/anyOf.ts b/lib/vocabularies/applicator/anyOf.ts index 01b623373..87153c613 100644 --- a/lib/vocabularies/applicator/anyOf.ts +++ b/lib/vocabularies/applicator/anyOf.ts @@ -1,7 +1,5 @@ import type {CodeKeywordDefinition, ErrorNoParams, AnySchema} from "../../types" -import type KeywordCxt from "../../compile/context" -import {_, not} from "../../compile/codegen" -import {alwaysValidSchema} from "../../compile/util" +import {validateUnion} from "../code" export type AnyOfError = ErrorNoParams<"anyOf", AnySchema[]> @@ -9,40 +7,7 @@ const def: CodeKeywordDefinition = { keyword: "anyOf", schemaType: "array", trackErrors: true, - code(cxt: KeywordCxt) { - const {gen, schema, it} = cxt - /* istanbul ignore if */ - if (!Array.isArray(schema)) throw new Error("ajv implementation error") - const alwaysValid = schema.some((sch: AnySchema) => alwaysValidSchema(it, sch)) - if (alwaysValid && !it.opts.unevaluated) return - - const valid = gen.let("valid", false) - const schValid = gen.name("_valid") - - gen.block(() => - schema.forEach((_sch: AnySchema, i: number) => { - const schCxt = cxt.subschema( - { - keyword: "anyOf", - schemaProp: i, - compositeRule: true, - }, - schValid - ) - gen.assign(valid, _`${valid} || ${schValid}`) - const merged = cxt.mergeValidEvaluated(schCxt, schValid) - // can short-circuit if `unevaluatedProperties/Items` not supported (opts.unevaluated !== true) - // or if all properties and items were evaluated (it.props === true && it.items === true) - if (!merged) gen.if(not(valid)) - }) - ) - - cxt.result( - valid, - () => cxt.reset(), - () => cxt.error(true) - ) - }, + code: validateUnion, error: { message: "should match some schema in anyOf", }, diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index 12c671b1b..3c4d3a1bd 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -113,3 +113,38 @@ export function validateArray(cxt: KeywordCxt): Name { }) } } + +export function validateUnion(cxt: KeywordCxt): void { + const {gen, schema, keyword, it} = cxt + /* istanbul ignore if */ + if (!Array.isArray(schema)) throw new Error("ajv implementation error") + const alwaysValid = schema.some((sch: AnySchema) => alwaysValidSchema(it, sch)) + if (alwaysValid && !it.opts.unevaluated) return + + const valid = gen.let("valid", false) + const schValid = gen.name("_valid") + + gen.block(() => + schema.forEach((_sch: AnySchema, i: number) => { + const schCxt = cxt.subschema( + { + keyword, + schemaProp: i, + compositeRule: true, + }, + schValid + ) + gen.assign(valid, _`${valid} || ${schValid}`) + const merged = cxt.mergeValidEvaluated(schCxt, schValid) + // can short-circuit if `unevaluatedProperties/Items` not supported (opts.unevaluated !== true) + // or if all properties and items were evaluated (it.props === true && it.items === true) + if (!merged) gen.if(not(valid)) + }) + ) + + cxt.result( + valid, + () => cxt.reset(), + () => cxt.error(true) + ) +} diff --git a/lib/vocabularies/jtd/enum.ts b/lib/vocabularies/jtd/enum.ts index df0690e09..aaf43393a 100644 --- a/lib/vocabularies/jtd/enum.ts +++ b/lib/vocabularies/jtd/enum.ts @@ -8,6 +8,7 @@ const def: CodeKeywordDefinition = { code(cxt: KeywordCxt) { const {gen, data, schema, schemaValue, parentSchema, it} = cxt if (schema.length === 0) throw new Error("enum must have non-empty array") + if (schema.length !== new Set(schema).size) throw new Error("enum items must be unique") let valid: Code if (schema.length >= it.opts.loopEnum) { if (parentSchema.nullable) { diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index d40736a71..e39a6bd94 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -1,4 +1,5 @@ import type {Vocabulary} from "../../types" +// import definitions from "./definitions" import refKeyword from "./ref" import typeKeyword from "./type" import enumKeyword from "./enum" @@ -7,6 +8,7 @@ import properties from "./properties" import optionalProperties from "./optionalProperties" import discriminator from "./discriminator" import values from "./values" +import union from "./union" const jtdVocabulary: Vocabulary = [ "definitions", @@ -18,6 +20,7 @@ const jtdVocabulary: Vocabulary = [ optionalProperties, discriminator, values, + union, {keyword: "additionalProperties", schemaType: "boolean"}, {keyword: "nullable", schemaType: "boolean"}, {keyword: "metadata", schemaType: "object"}, diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index e4b48a3fe..70db84e13 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -13,7 +13,11 @@ const def: CodeKeywordDefinition = { export function validateProperties(cxt: KeywordCxt): void { const {gen, data, parentSchema, it} = cxt - const {additionalProperties} = parentSchema + const {additionalProperties, nullable} = parentSchema + if (it.jtdDiscriminator && nullable) throw new Error("JTD: nullable inside discriminator mapping") + if (commonProperties()) { + throw new Error("JTD: properties and optionalProperties have common members") + } const [allProps, properties] = schemaProperties("properties") const [allOptProps, optProperties] = schemaProperties("optionalProperties") if (properties.length === 0 && optProperties.length === 0 && additionalProperties) { @@ -33,9 +37,22 @@ export function validateProperties(cxt: KeywordCxt): void { ) cxt.pass(valid) + function commonProperties(): boolean { + const props = parentSchema.properties as Record | undefined + const optProps = parentSchema.optionalProperties as Record | undefined + if (!(props && optProps)) return false + for (const p in props) { + if (Object.prototype.hasOwnProperty.call(optProps, p)) return true + } + return false + } + function schemaProperties(keyword: string): [string[], string[]] { const schema = parentSchema[keyword] const allPs = schema ? allSchemaProperties(schema) : [] + if (it.jtdDiscriminator && allPs.some((p) => p === it.jtdDiscriminator)) { + throw new Error(`JTD: discriminator tag used in ${keyword}`) + } const ps = allPs.filter((p) => !alwaysValidSchema(it, schema[p])) return [allPs, ps] } @@ -88,7 +105,12 @@ export function validateProperties(cxt: KeywordCxt): void { if (props.length > 8) { // TODO maybe an option instead of hard-coded 8? const propsSchema = schemaRefOrVal(it, parentSchema[keyword], keyword) - additional = _`!${propsSchema}.hasOwnProperty(${key})` + const hasProp = gen.scopeValue("func", { + // eslint-disable-next-line @typescript-eslint/unbound-method + ref: Object.prototype.hasOwnProperty, + code: _`Object.prototype.hasOwnProperty`, + }) + additional = _`!${hasProp}.call(${propsSchema}, ${key})` } else if (props.length) { additional = and(...props.map((p) => _`${key} !== ${p}`)) } else { diff --git a/lib/vocabularies/jtd/type.ts b/lib/vocabularies/jtd/type.ts index 081b0fef3..ac31c4e57 100644 --- a/lib/vocabularies/jtd/type.ts +++ b/lib/vocabularies/jtd/type.ts @@ -44,21 +44,6 @@ const def: CodeKeywordDefinition = { } cxt.pass(parentSchema.nullable ? or(_`${data} === null`, cond) : cond) }, - // metaSchema: { - // enum: [ - // "boolean", - // "timestamp", - // "string", - // "float32", - // "float64", - // "int8", - // "uint8", - // "int16", - // "uint16", - // "int32", - // "uint32", - // ], - // }, } export default def diff --git a/lib/vocabularies/jtd/union.ts b/lib/vocabularies/jtd/union.ts new file mode 100644 index 000000000..3e914a581 --- /dev/null +++ b/lib/vocabularies/jtd/union.ts @@ -0,0 +1,15 @@ +import {KeywordCxt} from "../../ajv" +import type {CodeKeywordDefinition} from "../../types" +import {validateUnion} from "../code" + +const def: CodeKeywordDefinition = { + keyword: "union", + schemaType: "array", + trackErrors: true, + code(cxt: KeywordCxt) { + // if (!cxt.it.schemaEnv.meta) throw new Error("JTD: union keyword is only allowed in meta-schema") + validateUnion(cxt) + }, +} + +export default def diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index b97172bd0..a20f1a54a 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -47,30 +47,27 @@ describe("JSON Type Definition", () => { for (const testName in jtdValidationTests) { const {schema, instance, errors} = jtdValidationTests[testName] as TestCase const valid = errors.length === 0 - describeOnly(testName, () => { - it(`should be ${valid ? "valid" : "invalid"}`, () => { + describeOnly(testName, () => + it(`should be ${valid ? "valid" : "invalid"}`, () => withStandalone(ajvs).forEach((ajv) => { // console.log(ajv.compile(schema).toString()) // console.log(ajv.validate(schema, instance), ajv.errors) assert.strictEqual(ajv.validate(schema, instance), valid) - }) - }) - }) + })) + ) } }) - describe.skip("invalid schemas", () => { + describe("invalid schemas", () => { let ajv: AjvJTD - before(() => { - ajv = new _AjvJTD() - }) + before(() => (ajv = new _AjvJTD())) for (const testName in jtdInvalidSchemasTests) { const schema = jtdInvalidSchemasTests[testName] - it(`${testName} should be invalid schema`, () => { - assert.throws(() => ajv.compile(schema)) - }) + describe(testName, () => + it("should be invalid schema", () => assert.throws(() => ajv.compile(schema))) + ) } }) }) From b2c8061996fa93a5f34c91e74613f1ee80b3ecc1 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 11:35:29 +0000 Subject: [PATCH 18/29] JTD: use metadata as location for user-defined keywords, allow union only inside metadata --- lib/compile/index.ts | 1 + lib/compile/subschema.ts | 12 +++++++- lib/refs/jtd-schema.ts | 43 ++++++++++++++------------- lib/vocabularies/jtd/discriminator.ts | 4 ++- lib/vocabularies/jtd/elements.ts | 2 ++ lib/vocabularies/jtd/enum.ts | 2 ++ lib/vocabularies/jtd/index.ts | 3 +- lib/vocabularies/jtd/metadata.ts | 24 +++++++++++++++ lib/vocabularies/jtd/properties.ts | 2 ++ lib/vocabularies/jtd/ref.ts | 2 ++ lib/vocabularies/jtd/type.ts | 2 ++ lib/vocabularies/jtd/union.ts | 6 +--- lib/vocabularies/jtd/values.ts | 2 ++ 13 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 lib/vocabularies/jtd/metadata.ts diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 0f6fa2eaf..a7cc30ced 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -52,6 +52,7 @@ export interface SchemaCxt { props?: EvaluatedProperties | Name // properties evaluated by this schema - used by parent schema or assigned to validation function items?: EvaluatedItems | Name // last item evaluated by this schema - used by parent schema or assigned to validation function jtdDiscriminator?: string + jtdMetadata?: boolean readonly createErrors?: boolean readonly opts: InstanceOptions // Ajv instance option. readonly self: Ajv // current Ajv instance diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index 7365b144e..fd0e971be 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -22,6 +22,7 @@ interface SubschemaContext { dataPathArr?: (Code | number)[] propertyName?: Name jtdDiscriminator?: string + jtdMetadata?: boolean compositeRule?: true createErrors?: boolean allErrors?: boolean @@ -46,6 +47,7 @@ export type SubschemaArgs = Partial<{ propertyName: Name dataPropType: Type jtdDiscriminator: string + jtdMetadata: boolean compositeRule: true createErrors: boolean allErrors: boolean @@ -147,12 +149,20 @@ function extendSubschemaData( function extendSubschemaMode( subschema: SubschemaContext, - {jtdDiscriminator, compositeRule, createErrors, allErrors, strictSchema}: SubschemaArgs + { + jtdDiscriminator, + jtdMetadata, + compositeRule, + createErrors, + allErrors, + strictSchema, + }: SubschemaArgs ): void { if (compositeRule !== undefined) subschema.compositeRule = compositeRule if (createErrors !== undefined) subschema.createErrors = createErrors if (allErrors !== undefined) subschema.allErrors = allErrors subschema.jtdDiscriminator = jtdDiscriminator // not inherited + subschema.jtdMetadata = jtdMetadata // not inherited subschema.strictSchema = strictSchema // not inherited } diff --git a/lib/refs/jtd-schema.ts b/lib/refs/jtd-schema.ts index 6a79f5c56..c01981289 100644 --- a/lib/refs/jtd-schema.ts +++ b/lib/refs/jtd-schema.ts @@ -5,7 +5,12 @@ type MetaSchema = (root: boolean) => SchemaObject const shared: MetaSchema = (root) => { const sch: SchemaObject = { nullable: {type: "boolean"}, - metadata: {optionalProperties: {}, additionalProperties: true}, + metadata: { + optionalProperties: { + union: {elements: {ref: "schema"}}, + }, + additionalProperties: true, + }, } if (root) sch.definitions = {values: {ref: "schema"}} return sch @@ -83,7 +88,9 @@ const discriminatorForm: MetaSchema = (root) => ({ discriminator: {type: "string"}, mapping: { values: { - union: [propertiesForm(false), optionalPropertiesForm(false)], + metadata: { + union: [propertiesForm(false), optionalPropertiesForm(false)], + }, }, }, }, @@ -97,26 +104,20 @@ const valuesForm: MetaSchema = (root) => ({ optionalProperties: shared(root), }) -const unionForm: MetaSchema = (root) => ({ - properties: { - union: {elements: {ref: "schema"}}, - }, - optionalProperties: shared(root), -}) - const schema: MetaSchema = (root) => ({ - union: [ - emptyForm, - refForm, - typeForm, - enumForm, - elementsForm, - propertiesForm, - optionalPropertiesForm, - discriminatorForm, - valuesForm, - unionForm, - ].map((s) => s(root)), + metadata: { + union: [ + emptyForm, + refForm, + typeForm, + enumForm, + elementsForm, + propertiesForm, + optionalPropertiesForm, + discriminatorForm, + valuesForm, + ].map((s) => s(root)), + }, }) const jtdMetaSchema: SchemaObject = { diff --git a/lib/vocabularies/jtd/discriminator.ts b/lib/vocabularies/jtd/discriminator.ts index 98b3bd6e4..92dde8278 100644 --- a/lib/vocabularies/jtd/discriminator.ts +++ b/lib/vocabularies/jtd/discriminator.ts @@ -1,13 +1,15 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" -import {checkNullableObject} from "./nullable" import {_, getProperty, Name} from "../../compile/codegen" +import {checkMetadata} from "./metadata" +import {checkNullableObject} from "./nullable" const def: CodeKeywordDefinition = { keyword: "discriminator", schemaType: "string", implements: ["mapping"], code(cxt: KeywordCxt) { + checkMetadata(cxt) const {gen, data, schema, parentSchema, it} = cxt const [valid, cond] = checkNullableObject(cxt, data) diff --git a/lib/vocabularies/jtd/elements.ts b/lib/vocabularies/jtd/elements.ts index dec6bfb11..d67f2cd6e 100644 --- a/lib/vocabularies/jtd/elements.ts +++ b/lib/vocabularies/jtd/elements.ts @@ -3,12 +3,14 @@ import type KeywordCxt from "../../compile/context" import {alwaysValidSchema} from "../../compile/util" import {validateArray} from "../code" import {_, and, nil} from "../../compile/codegen" +import {checkMetadata} from "./metadata" import {checkNullable} from "./nullable" const def: CodeKeywordDefinition = { keyword: "elements", schemaType: "object", code(cxt: KeywordCxt) { + checkMetadata(cxt) const {gen, data, schema, it} = cxt if (alwaysValidSchema(it, schema)) return const [valid, cond] = checkNullable(cxt, nil) diff --git a/lib/vocabularies/jtd/enum.ts b/lib/vocabularies/jtd/enum.ts index aaf43393a..dd582a140 100644 --- a/lib/vocabularies/jtd/enum.ts +++ b/lib/vocabularies/jtd/enum.ts @@ -1,11 +1,13 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, or, not, Code} from "../../compile/codegen" +import {checkMetadata} from "./metadata" const def: CodeKeywordDefinition = { keyword: "enum", schemaType: "array", code(cxt: KeywordCxt) { + checkMetadata(cxt) const {gen, data, schema, schemaValue, parentSchema, it} = cxt if (schema.length === 0) throw new Error("enum must have non-empty array") if (schema.length !== new Set(schema).size) throw new Error("enum items must be unique") diff --git a/lib/vocabularies/jtd/index.ts b/lib/vocabularies/jtd/index.ts index e39a6bd94..2a241f61b 100644 --- a/lib/vocabularies/jtd/index.ts +++ b/lib/vocabularies/jtd/index.ts @@ -9,6 +9,7 @@ import optionalProperties from "./optionalProperties" import discriminator from "./discriminator" import values from "./values" import union from "./union" +import metadata from "./metadata" const jtdVocabulary: Vocabulary = [ "definitions", @@ -21,9 +22,9 @@ const jtdVocabulary: Vocabulary = [ discriminator, values, union, + metadata, {keyword: "additionalProperties", schemaType: "boolean"}, {keyword: "nullable", schemaType: "boolean"}, - {keyword: "metadata", schemaType: "object"}, ] export default jtdVocabulary diff --git a/lib/vocabularies/jtd/metadata.ts b/lib/vocabularies/jtd/metadata.ts new file mode 100644 index 000000000..19eeb8c7d --- /dev/null +++ b/lib/vocabularies/jtd/metadata.ts @@ -0,0 +1,24 @@ +import {KeywordCxt} from "../../ajv" +import type {CodeKeywordDefinition} from "../../types" +import {alwaysValidSchema} from "../../compile/util" + +const def: CodeKeywordDefinition = { + keyword: "metadata", + schemaType: "object", + code(cxt: KeywordCxt) { + checkMetadata(cxt) + const {gen, schema, it} = cxt + if (alwaysValidSchema(it, schema)) return + const valid = gen.name("valid") + cxt.subschema({keyword: "metadata", jtdMetadata: true}, valid) + cxt.ok(valid) + }, +} + +export function checkMetadata({it, keyword}: KeywordCxt, metadata?: boolean): void { + if (it.jtdMetadata !== metadata) { + throw new Error(`JTD: "${keyword}" cannot be used in this schema location`) + } +} + +export default def diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 70db84e13..92f943c5c 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -3,6 +3,7 @@ import type KeywordCxt from "../../compile/context" import {propertyInData, allSchemaProperties} from "../code" import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util" import {_, and, Code, Name} from "../../compile/codegen" +import {checkMetadata} from "./metadata" import {checkNullableObject} from "./nullable" const def: CodeKeywordDefinition = { @@ -12,6 +13,7 @@ const def: CodeKeywordDefinition = { } export function validateProperties(cxt: KeywordCxt): void { + checkMetadata(cxt) const {gen, data, parentSchema, it} = cxt const {additionalProperties, nullable} = parentSchema if (it.jtdDiscriminator && nullable) throw new Error("JTD: nullable inside discriminator mapping") diff --git a/lib/vocabularies/jtd/ref.ts b/lib/vocabularies/jtd/ref.ts index c1a46778a..929a3cb36 100644 --- a/lib/vocabularies/jtd/ref.ts +++ b/lib/vocabularies/jtd/ref.ts @@ -5,11 +5,13 @@ import {_, not, nil, stringify} from "../../compile/codegen" import {MissingRefError} from "../../compile/error_classes" import N from "../../compile/names" import {getValidate, callRef} from "../core/ref" +import {checkMetadata} from "./metadata" const def: CodeKeywordDefinition = { keyword: "ref", schemaType: "string", code(cxt: KeywordCxt) { + checkMetadata(cxt) const {gen, data, schema: ref, parentSchema, it} = cxt const { schemaEnv: {root}, diff --git a/lib/vocabularies/jtd/type.ts b/lib/vocabularies/jtd/type.ts index ac31c4e57..86cecffd0 100644 --- a/lib/vocabularies/jtd/type.ts +++ b/lib/vocabularies/jtd/type.ts @@ -2,6 +2,7 @@ import type {CodeKeywordDefinition} from "../../types" import type KeywordCxt from "../../compile/context" import {_, or, Code} from "../../compile/codegen" import validTimestamp from "../../compile/timestamp" +import {checkMetadata} from "./metadata" type IntType = "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" @@ -18,6 +19,7 @@ const def: CodeKeywordDefinition = { keyword: "type", schemaType: "string", code(cxt: KeywordCxt) { + checkMetadata(cxt) const {gen, data, schema, parentSchema} = cxt let cond: Code switch (schema) { diff --git a/lib/vocabularies/jtd/union.ts b/lib/vocabularies/jtd/union.ts index 3e914a581..bd6f142a5 100644 --- a/lib/vocabularies/jtd/union.ts +++ b/lib/vocabularies/jtd/union.ts @@ -1,4 +1,3 @@ -import {KeywordCxt} from "../../ajv" import type {CodeKeywordDefinition} from "../../types" import {validateUnion} from "../code" @@ -6,10 +5,7 @@ const def: CodeKeywordDefinition = { keyword: "union", schemaType: "array", trackErrors: true, - code(cxt: KeywordCxt) { - // if (!cxt.it.schemaEnv.meta) throw new Error("JTD: union keyword is only allowed in meta-schema") - validateUnion(cxt) - }, + code: validateUnion, } export default def diff --git a/lib/vocabularies/jtd/values.ts b/lib/vocabularies/jtd/values.ts index ba14015e7..96488c4b2 100644 --- a/lib/vocabularies/jtd/values.ts +++ b/lib/vocabularies/jtd/values.ts @@ -3,12 +3,14 @@ import type KeywordCxt from "../../compile/context" import {Type} from "../../compile/subschema" import {alwaysValidSchema} from "../../compile/util" import {not, Name} from "../../compile/codegen" +import {checkMetadata} from "./metadata" import {checkNullableObject} from "./nullable" const def: CodeKeywordDefinition = { keyword: "values", schemaType: "object", code(cxt: KeywordCxt) { + checkMetadata(cxt) const {gen, data, schema, it} = cxt if (alwaysValidSchema(it, schema)) return const [valid, cond] = checkNullableObject(cxt, data) From 0760f82d78ee8cee8656a8d2ed6c24d90dfa963f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 12:18:59 +0000 Subject: [PATCH 19/29] fix npm build script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 125391cb1..04512bdc9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test-debug": "npm run test-spec -- --inspect-brk", "test-cov": "nyc npm run test-spec", "bundle": "rm -rf bundle && node ./scripts/bundle.js ajv ajv7 ajv7 && node ./scripts/bundle.js 2019 ajv2019 ajv2019 && node ./scripts/bundle.js jtd ajvJTD ajvJTD", - "build": "rm -rf dist && tsc && cp -r lib/refs dist && rm dist/refs/json-schema-2019-09/index.ts", + "build": "rm -rf dist && tsc && cp -r lib/refs dist && rm dist/refs/json-schema-2019-09/index.ts && rm dist/refs/jtd-schema.ts", "json-tests": "rm -rf spec/_json/*.js && node scripts/jsontests", "test-karma": "karma start", "test-browser": "rm -rf .browser && npm run bundle && scripts/prepare-tests && karma start", From 6a4124d5647dca0783e592aaea816d2b64bd7439 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 12:32:41 +0000 Subject: [PATCH 20/29] test: increase timeout to 10s in JTD test --- spec/jtd-schema.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index a20f1a54a..45a0ca5d0 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -33,7 +33,8 @@ interface TestCaseError { const ONLY: RegExp[] = [] describe("JSON Type Definition", () => { - describe("validation", () => { + describe("validation", function () { + this.timeout(10000) let ajvs: AjvJTD[] before(() => { From 2e2f4d11e32f37cd3d0191387a64121956d97143 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 13:15:57 +0000 Subject: [PATCH 21/29] docs for JTD (WIP) --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 65368f5fe..256465050 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ Ajv logo -# Ajv: Another JSON Schema Validator +# Ajv: Another JSON schema validator -The fastest JSON Schema validator for Node.js and browser. Supports draft-06/07/2019-09 (draft-04 is supported in [version 6](https://github.com/ajv-validator/ajv/tree/v6)). +The fastest JSON schema validator for Node.js and browser. Supports JSON Schema draft-06/07/2019-09 (draft-04 is supported in [version 6](https://github.com/ajv-validator/ajv/tree/v6)) and JSON Type Definition [RFC8927](https://datatracker.ietf.org/doc/rfc8927/). [![build](https://github.com/ajv-validator/ajv/workflows/build/badge.svg)](https://github.com/ajv-validator/ajv/actions?query=workflow%3Abuild) [![npm](https://img.shields.io/npm/v/ajv.svg)](https://www.npmjs.com/package/ajv) @@ -17,9 +17,10 @@ The fastest JSON Schema validator for Node.js and browser. Supports draft-06/07/ ## Using version 7 -Ajv version 7 is released with these changes: +Ajv version 7 has these new features: - support of JSON Schema draft-2019-09 features: [`unevaluatedProperties`](./docs/json-schema.md#unevaluatedproperties) and [`unevaluatedItems`](./docs/json-schema.md#unevaluateditems), [dynamic recursive references](./docs/validation.md#extending-recursive-schemas) and other [additional keywords](./docs/json-schema.md#json-schema-draft-2019-09). +- NEW: support of JSON Type Definition [RFC8927](https://datatracker.ietf.org/doc/rfc8927/) (from [v7.1.0](https://github.com/ajv-validator/ajv-keywords/releases/tag/v7.1.0)) - to reduce the mistakes in JSON schemas and unexpected validation results, [strict mode](./docs/strict-mode.md) is added - it prohibits ignored or ambiguous JSON Schema elements. - to make code injection from untrusted schemas impossible, [code generation](./docs/codegen.md) is fully re-written to be safe and to allow code optimization (compiled schema code size is reduced by more than 10%). - to simplify Ajv extensions, the new keyword API that is used by pre-defined keywords is available to user-defined keywords - it is much easier to define any keywords now, especially with subschemas. [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package was updated to use the new API (in [v4.0.0](https://github.com/ajv-validator/ajv-keywords/releases/tag/v4.0.0)) @@ -71,6 +72,7 @@ Please review [Contributing guidelines](./CONTRIBUTING.md) and [Code components] - [Performance](#performance) - [Features](#features) - [Getting started](#usage) +- [Choosing schema language: JSON Schema vs JSON Type Definition](#choosing-schema-language) - [Frequently Asked Questions](./docs/faq.md) - [Using in browser](#using-in-browser) - [Content Security Policy](./docs/security.md#content-security-policy) @@ -166,7 +168,7 @@ Performance of different validators by [json-schema-benchmark](https://github.co ## Features -- Ajv implements full JSON Schema [draft-06/07](http://json-schema.org/) standards (draft-04 is supported in v6): +- Ajv implements JSON Schema [draft-06/07/2019-09](http://json-schema.org/) standards (draft-04 is supported in v6): - all validation keywords (see [JSON Schema validation keywords](./docs/json-schema.md)) - keyword "nullable" from [Open API 3 specification](https://swagger.io/docs/specification/data-models/data-types/). - full support of remote references (remote schemas have to be added with `addSchema` or compiled to be available) @@ -174,6 +176,10 @@ Performance of different validators by [json-schema-benchmark](https://github.co - correct string lengths for strings with unicode pairs - [formats](#formats) defined by JSON Schema draft-07 standard (with [ajv-formats](https://github.com/ajv-validator/ajv-formats) plugin) and additional formats (can be turned off) - [validates schemas against meta-schema](./docs/api.md#api-validateschema) +- NEW: supports [JSON Type Definition](https://datatracker.ietf.org/doc/rfc8927/): + - all forms (see [JSON Type Definition schema forms](./docs/json-type-definition.md)) + - meta-schema for JTD schemas + - "union" keyword and user-defined keywords (can be used inside "metadata" member of the schema) - supports [browsers](#using-in-browser) and Node.js 0.10-14.x - [asynchronous loading](./docs/validation.md#asynchronous-schema-compilation) of referenced schemas during compilation - "All errors" validation mode with [option allErrors](./docs/api.md#options) @@ -262,6 +268,26 @@ if (validate(data)) { } ``` +With JSON Type Definition schema: + +```javascript +const Ajv = require("ajv").default + +const ajv = new Ajv() // options can be passed, e.g. {allErrors: true} +const schema = { + properties: { + foo: {type: "float64"} + } +} +const validate = ajv.compile(schema) +const valid = validate({foo: 1}) // true +if (!valid) console.log(validate.errors) +// Unlike JSON Schema: +const valid1 = validate(1) // false, bot an object +const valid2 = validate({}) // false, foo is required +const valid3 = validate({foo: 1, bar: 2}) // false, bar is additional +``` + See [this test](./spec/types/json-schema.spec.ts) for an advanced example, [API reference](./docs/api.md) and [Options](./docs/api.md#options) for more details. Ajv compiles schemas to functions and caches them in all cases (using schema itself as a key for Map) or another function passed via options), so that the next time the same schema is used (not necessarily the same object instance) it won't be compiled again. @@ -290,7 +316,7 @@ Then you need to load Ajv with support of JSON Schema draft-07 in the browser: ``` -or to load the bundle that supports JSONSchema draft-2019-09: +To load the bundle that supports JSON Schema draft-2019-09: ```html @@ -302,12 +328,28 @@ or to load the bundle that supports JSONSchema draft-2019-09: ``` +To load the bundle that supports JSON Type Definition: + +```html + + +``` + This bundle can be used with different module systems; it creates global `ajv` (or `ajv2019`) if no module system is found. The browser bundle is available on [cdnjs](https://cdnjs.com/libraries/ajv). **Please note**: some frameworks, e.g. Dojo, may redefine global require in a way that is not compatible with CommonJS module format. In this 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)). +## Choosing schema language + +Which schema language should you choose: JSON Schema vs JSON Type Definition? + ## Using in ES5 environment You need to: From ad75a12fb03710ca3290cb13c9b6ef03dd53098d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 18:13:14 +0000 Subject: [PATCH 22/29] JTD docs --- README.md | 59 +++++++- docs/json-type-definition.md | 286 +++++++++++++++++++++++++++++++++++ docs/validation.md | 12 +- 3 files changed, 346 insertions(+), 11 deletions(-) create mode 100644 docs/json-type-definition.md diff --git a/README.md b/README.md index 256465050..8b3ebf509 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,9 @@ Please review [Contributing guidelines](./CONTRIBUTING.md) and [Code components] - [Performance](#performance) - [Features](#features) - [Getting started](#usage) -- [Choosing schema language: JSON Schema vs JSON Type Definition](#choosing-schema-language) +- [Choosing schema language](#choosing-schema-language) + - [JSON Schema](#json-schema) + - [JSON Type Definition](#json-type-definition) - [Frequently Asked Questions](./docs/faq.md) - [Using in browser](#using-in-browser) - [Content Security Policy](./docs/security.md#content-security-policy) @@ -276,8 +278,8 @@ const Ajv = require("ajv").default const ajv = new Ajv() // options can be passed, e.g. {allErrors: true} const schema = { properties: { - foo: {type: "float64"} - } + foo: {type: "float64"}, + }, } const validate = ajv.compile(schema) const valid = validate({foo: 1}) // true @@ -348,7 +350,54 @@ The browser bundle is available on [cdnjs](https://cdnjs.com/libraries/ajv). ## Choosing schema language -Which schema language should you choose: JSON Schema vs JSON Type Definition? +Both JSON Schema and JSON Type Definition are cross-platform specifications with implementations in multiple programming languages that help you define the shape and requirements to your JSON data. + +This section compares their pros/cons to help decide which specification fits your application better. + +### JSON Schema + +- Pros + - Support of complex validation scenarios: + - untagged unions and boolean logic + - conditional schemas and dependencies + - restrictions on number ranges and string, array and object sizes + - semantic validation with formats, patterns and content keywords + - distribute strict record definitions across multiple schemas (with unevaluatedProperties) + - Wide specification adoption. + - Used as part of OpenAPI specification. + - Can be effectively used for validation of any JavaScript objects and configuration files. +- Cons + - Defines the collection of restrictions on your data, rather than the shape of the data. + - No standard support for tagged unions. + - Complex, error prone and often confusing for the new users (Ajv has [strict mode](./docs/strict-mode) to compensate for it, but it is not cross-platform). + - Some parts of specification are difficult to implement, creating the risk of divergence of implementations: + - reference resolution model + - unevaluatedProperties/unevaluatedItems + - recursive references + - Internet draft status (rather than RFC) + +See [JSON Schema](./docs/json-schema.md) for the list of defined keywords. + +### JSON Type Definition + +- Pros: + - Aligned with type systems of many languages - can be used to generate type definitions and efficient parsers and serializers to/from these types. + - Extremely simple, enforcing best practices for cross-platform JSON API modelling. + - Simple to implement, ensuring consistency across implementations. + - Defines the shape of JSON data via strictly defined schema forms (rather than the collection of restrictions). + - Effective support for tagged unions. + - Designed to protect against user mistakes. + - Approved as [RFC8927](https://datatracker.ietf.org/doc/rfc8927/) +- Cons: + - Limited, compared with JSON Schema - no untagged unions\*, conditionals, references between schemas\*\*, etc. + - No meta-schema in the specification\*. + - Brand new - limited industry adoption (as of January 2021). + +\* Ajv defines meta-schema for JTD schemas and non-standard keyword "union" that can be used inside "metadata" object. + +\*\* You can still combine schemas in JavaScript code. + +See [JSON Type Definition](./docs/json-type-definition.md) for the list of defined schema forms. ## Using in ES5 environment @@ -376,7 +425,7 @@ CLI is available as a separate npm package [ajv-cli](https://github.com/ajv-vali - user-defined meta-schemas, validation keywords and formats - 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 +- reporting changes in data after validation in [JSON-patch](https://datatracker.ietf.org/doc/rfc6902/) format ## Extending Ajv diff --git a/docs/json-type-definition.md b/docs/json-type-definition.md new file mode 100644 index 000000000..dfa0257cd --- /dev/null +++ b/docs/json-type-definition.md @@ -0,0 +1,286 @@ +# JSON Type Definition + +This document informally describes JSON Type Definitions specification to help Ajv users to start using it. For formal definition please refer to [RFC8927](https://datatracker.ietf.org/doc/rfc8927/). Please report any contradictions in this document with the specification. + +JTD specification defines 8 different forms that the schema for JSON can take for one of most widely used data types in JSON messages (API requests and responses). + +All forms require that: + +- schema is an object with different members, depending on the form +- each form can have: + - an optional member `nullable` with a boolean value that allows data instance to be JSON `null`. + - an optional member `metadata` with an object value that allows to pass any additional information or extend the specification (Ajv defines keyword "union" that can be used inside `metadata`) + +Root schema can have member `definitions` that has a dictionary of schemas that can be references from any other schemas using form `ref` + +## Type schema form + +This form defines a primitive value. + +It has a required member `type` and an optional members `nullable` and `metadata`, no other members are allowed. + +`type` can have one of the following values: + +- `"string"` - defines a string +- `"boolean"` - defines boolean value `true` or `false` +- `"timestamp"` - defines timestamp (JSON string, Ajv would also allow Date object with this type) according to [RFC3339](https://datatracker.ietf.org/doc/rfc3339/) +- `type` values that define integer numbers: + - `"int8"` - signed byte value (-128 .. 127) + - `"uint8"` - unsigned byte value (0 .. 255) + - `"int16"` - signed word value (-32768 .. 32767), + - `"uint16"` - unsigned word value (0 .. 65535) + - `"int32"` - signed 32-bit integer value + - `"uint32"` - unsigned 32-bit integer value + `type` values that define floating point numbers: + - `"float32"` - 32-bit real number + - `"float64"` - 64-bit real number + +Unlike JSON Schema, JTD does not allow defining values that can take one of several types, but they can be defined as `nullable`. + +**Example** + +```javascript +{ + type: "string" +} +``` + +## Enum schema form + +This form defines a string that can take one of the values from the list (the values in the list must be unique). + +It has a required member `enum` and optional members `nullable` and `metadata`, no other members are allowed. + +Unlike JSON Schema, JTD does not allow defining `enum` with values of any other type than string. + +**Example** + +```javascript +{ + enum: ["foo", "bar"] +} +``` + +## Elements schema form + +This form defines a homogenous array of any size (possibly empty) with the elements that satisfy a given schema. + +It has a required member `elements` (schema that elements should satisfy) and optional members `nullable` and `metadata`, no other members are allowed. + +Unlike JSON Schema, the data instance must be JSON array (without using additional `type` keyword), and there is no way to enforce the restrictions that cannot be present on type level of most languages, such as array size and uniqueness of items. + +**Example** + +Schema: + +```javascript +{ + elements: { + type: "string" + } +} +``` + +Valid data: `[]`, `["foo"]`, `["foo", "bar"]` + +Invalid data: `["foo", 1]`, any type other than array + +## Properties schema form + +This form defines record (JSON object) that has defined required and optional properties. + +It is required that this form has either `properties` member, or `optionalProperties`, or both, in which case the cannot have overlapping properties. Additional properties can be allowed by adding an optional boolean member `additionalProperties` with a value `true`. This form, as all other, can have optional `nullable` and `metadata` members. + +Unlike JSON Schema, all properties defined in `properties` schema member are required, the data instance must be JSON object (without using additional `type` keyword) and by default additional properties are not allowed (with the exception of discriminator tag - see the next section). This strictness minimises user mistakes. + +**Example 1.** + +Schema: + +```javascript +{ + properties: { + foo: { + type: "string" + } + } +} +``` + +Valid data: `{foo: "bar"}` + +Invalid data: `{}`, `{foo: 1}`, `{foo: "bar", bar: 1}`, any type other than object + +**Example 2.** + +Schema: + +```javascript +{ + properties: { + foo: {type: "string"} + }, + optionalProperties: { + bar: {enum: ["1", "2"]} + }, + additionalProperties: true +} +``` + +Valid data: `{foo: "bar"}`, `{foo: "bar", bar: "1"}`, `{foo: "bar", additional: 1}` + +Invalid data: `{}`, `{foo: 1}`, `{foo: "bar", bar: "3"}`, any type other than object + +**Example 3: invalid schema (overlapping required and optional properties)** + +```javascript +{ + properties: { + foo: {type: "string"} + }, + optionalProperties: { + foo: {type: "string"} + } +} +``` + +## Discriminator schema form + +This form defines discriminated (tagged) union of different record types. + +It has required members `discriminator` and `mappings` and optional members `nullable` and `metadata`, no other members are allowed. + +The string value of `discriminator` schema member contains the name of the data member that is the tag of the union. `mappings` schema member contains the dictionary of schemas that are applied according to the value of the tag member in the data. Schemas inside `mappings` must have "properties" form. + +Properties forms inside `mappings` cannot be `nullable` and cannot define the same property as discriminator tag. + +**Example 1.** + +Schema: + +```javascript +{ + discriminator: "version", + mappings: { + "1": { + properties: { + foo: {type: "string"} + } + }, + "2": { + properties: { + foo: {type: "uint8"} + } + } + } +} +``` + +Valid data: `{version: "1", foo: "1"}`, `{version: "2", foo: 1}` + +Invalid data: `{}`, `{foo: "1"}`, `{version: 1, foo: "1"}`, any type other than object + +**Example 3: invalid schema (discriminator tag member defined in mappings)** + +```javascript +{ + discriminator: "version", + mappings: { + "1": { + properties: { + version: {enum: ["1"]}, + foo: {type: "string"} + } + }, + "2": { + properties: { + version: {enum: ["2"]}, + foo: {type: "uint8"} + } + } + } +} +``` + +## Values schema form + +This form defines a homogenous dictionary where the values of members satisfy a given schema. + +It has a required member `values` (schema that member values should satisfy) and optional members `nullable` and `metadata`, no other members are allowed. + +Unlike JSON Schema, the data instance must be JSON object (without using additional `type` keyword), and there is no way to enforce size restrictions. + +**Example** + +Schema: + +```javascript +{ + values: { + type: "uint8" + } +} +``` + +Valid data: `{}`, `{"foo": 1}`, `{"foo": 1, "bar": 2}` + +Invalid data: `{"foo": "bar"}`, any type other than object + +## Ref schema form + +This form defines a reference to the schema that is present in the corresponding key in the `definitions` member of the root schema. + +It has a required member `ref` (member of `definitions` object in the root schema) and optional members `nullable` and `metadata`, no other members are allowed. + +Unlike JSON Schema, JTD does not allow to reference: + +- any schema fragment other than root level `definitions` member +- root of the schema - there is another way to define a self-recursive schema (see Example 2) +- another schema file (but you can still combine schemas from multiple files using JavaScript). + +**Example 1.** + +```javascript +{ + properties: { + propFoo: {ref: "foo", nullable: true} + }, + definitions: { + foo: {type: "string"} + } +} +``` + +**Example 2: self-referencing schema for binary tree** + +```javascript +{ + ref: "tree", + definitions: { + tree: { + properties: { + value: {type: "int32"} + }, + optionalProperties: { + left: {ref: "tree"}, + right: {ref: "tree"} + } + } + } +} +``` + +**Example 3: invalid schema (missing reference)** + +```javascript +{ + ref: "foo", + definitions: { + bar: {type: "string"} + } +} +``` + +## Empty schema form + +Empty JTD schema defines the data instance that can be of any type, including JSON `null` (even if `nullable` member is not present). It cannot have any member other than `nullable` and `metadata`. diff --git a/docs/validation.md b/docs/validation.md index 0cce94f3d..1a43c374f 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -101,7 +101,7 @@ JSON Schema specification defines several metadata keywords that describe the sc - `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". +- `contentMediaType`: [RFC 2046](https://datatracker.ietf.org/doc/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 any of them, it should remove these keywords from the instance. @@ -133,15 +133,15 @@ The following formats are defined in [ajv-formats](https://github.com/ajv-valida - _duration_: duration from [RFC3339](https://tools.ietf.org/html/rfc3339#appendix-A) - _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) +- _uri-template_: URI template according to [RFC6570](https://datatracker.ietf.org/doc/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. - _ipv6_: IP address v6. - _regex_: tests whether a string is a valid regular expression by passing it to RegExp constructor. -- _uuid_: Universally Unique IDentifier according to [RFC4122](http://tools.ietf.org/html/rfc4122). -- _json-pointer_: JSON-pointer according to [RFC6901](https://tools.ietf.org/html/rfc6901). +- _uuid_: Universally Unique Identifier according to [RFC4122](https://datatracker.ietf.org/doc/rfc4122/). +- _json-pointer_: JSON-pointer according to [RFC6901](https://datatracker.ietf.org/doc/rfc6901/). - _relative-json-pointer_: relative JSON-pointer according to [this draft](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00). **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. These formats are available in [ajv-formats-draft2019](https://github.com/luzlab/ajv-formats-draft2019) plugin. @@ -275,7 +275,7 @@ With `$data` option you can use values from the validated data as the values for `$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. -The value of "$data" should be a [JSON-pointer](https://tools.ietf.org/html/rfc6901) to the data (the root is always the top level data object, even if the $data reference is inside a referenced subschema) or a [relative JSON-pointer](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (it is relative to the current point in data; if the \$data reference is inside a referenced subschema it cannot point to the data outside of the root level for this subschema). +The value of "$data" should be a [JSON-pointer](https://datatracker.ietf.org/doc/rfc6901/) to the data (the root is always the top level data object, even if the $data reference is inside a referenced subschema) or a [relative JSON-pointer](http://tools.ietf.org/html/draft-luff-relative-json-pointer-00) (it is relative to the current point in data; if the \$data reference is inside a referenced subschema it cannot point to the data outside of the root level for this subschema). Examples. @@ -322,7 +322,7 @@ const validData = { ### $merge and $patch keywords -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). +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://datatracker.ietf.org/doc/rfc7396/) and [JSON Patch (RFC 6902)](https://datatracker.ietf.org/doc/rfc6902/). To add keywords `$merge` and `$patch` to Ajv instance use this code: From f6d461ccf1d90f8ecb33b302f41cabde7319a03b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 7 Feb 2021 19:18:46 +0000 Subject: [PATCH 23/29] remove unused strictSchema member of SchemaCxt --- lib/compile/index.ts | 2 -- lib/compile/subschema.ts | 23 ++----------------- .../applicator/additionalProperties.ts | 1 - lib/vocabularies/applicator/items.ts | 1 - .../applicator/patternProperties.ts | 1 - lib/vocabularies/applicator/properties.ts | 1 - lib/vocabularies/applicator/propertyNames.ts | 1 - lib/vocabularies/code.ts | 1 - lib/vocabularies/core/ref.ts | 1 - lib/vocabularies/jtd/discriminator.ts | 3 +-- lib/vocabularies/jtd/properties.ts | 1 - lib/vocabularies/jtd/ref.ts | 1 - lib/vocabularies/jtd/values.ts | 1 - .../unevaluated/unevaluatedProperties.ts | 1 - 14 files changed, 3 insertions(+), 36 deletions(-) diff --git a/lib/compile/index.ts b/lib/compile/index.ts index a7cc30ced..79c9101bb 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -38,7 +38,6 @@ export interface SchemaCxt { readonly ValidationError?: Name readonly schema: AnySchema // current schema object - equal to parentSchema passed via KeywordCxt readonly schemaEnv: SchemaEnv - readonly strictSchema?: boolean readonly rootId: string baseId: string // the current schema base URI that should be used as the base for resolving URIs in references (\$ref) readonly schemaPath: Code // the run-time expression that evaluates to the property name of the current schema @@ -137,7 +136,6 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { ValidationError: _ValidationError, schema: sch.schema, schemaEnv: sch, - strictSchema: true, rootId, baseId: sch.baseId || rootId, schemaPath: nil, diff --git a/lib/compile/subschema.ts b/lib/compile/subschema.ts index fd0e971be..b31bce260 100644 --- a/lib/compile/subschema.ts +++ b/lib/compile/subschema.ts @@ -8,7 +8,6 @@ import {JSONType} from "./rules" interface SubschemaContext { // TODO use Optional? align with SchemCxt property types schema: AnySchema - strictSchema?: boolean schemaPath: Code errSchemaPath: string topSchemaRef?: Code @@ -37,7 +36,6 @@ export type SubschemaArgs = Partial<{ keyword: string schemaProp: string | number schema: AnySchema - strictSchema: boolean schemaPath: Code errSchemaPath: string topSchemaRef: Code @@ -64,15 +62,7 @@ export function applySubschema(it: SchemaObjCxt, appl: SubschemaArgs, valid: Nam function getSubschema( it: SchemaObjCxt, - { - keyword, - schemaProp, - schema, - strictSchema, - schemaPath, - errSchemaPath, - topSchemaRef, - }: SubschemaArgs + {keyword, schemaProp, schema, schemaPath, errSchemaPath, topSchemaRef}: SubschemaArgs ): SubschemaContext { if (keyword !== undefined && schema !== undefined) { throw new Error('both "keyword" and "schema" passed, only one allowed') @@ -99,7 +89,6 @@ function getSubschema( } return { schema, - strictSchema, schemaPath, topSchemaRef, errSchemaPath, @@ -149,21 +138,13 @@ function extendSubschemaData( function extendSubschemaMode( subschema: SubschemaContext, - { - jtdDiscriminator, - jtdMetadata, - compositeRule, - createErrors, - allErrors, - strictSchema, - }: SubschemaArgs + {jtdDiscriminator, jtdMetadata, compositeRule, createErrors, allErrors}: SubschemaArgs ): void { if (compositeRule !== undefined) subschema.compositeRule = compositeRule if (createErrors !== undefined) subschema.createErrors = createErrors if (allErrors !== undefined) subschema.allErrors = allErrors subschema.jtdDiscriminator = jtdDiscriminator // not inherited subschema.jtdMetadata = jtdMetadata // not inherited - subschema.strictSchema = strictSchema // not inherited } function getErrorPath( diff --git a/lib/vocabularies/applicator/additionalProperties.ts b/lib/vocabularies/applicator/additionalProperties.ts index 95f4d0b2f..381495a5a 100644 --- a/lib/vocabularies/applicator/additionalProperties.ts +++ b/lib/vocabularies/applicator/additionalProperties.ts @@ -107,7 +107,6 @@ const def: CodeKeywordDefinition & AddedKeywordDefinition = { keyword: "additionalProperties", dataProp: key, dataPropType: Type.Str, - strictSchema: it.strictSchema, } if (errors === false) { Object.assign(subschema, { diff --git a/lib/vocabularies/applicator/items.ts b/lib/vocabularies/applicator/items.ts index 3be134ac3..9b46b46b5 100644 --- a/lib/vocabularies/applicator/items.ts +++ b/lib/vocabularies/applicator/items.ts @@ -39,7 +39,6 @@ const def: CodeKeywordDefinition = { keyword: "items", schemaProp: i, dataProp: i, - strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/applicator/patternProperties.ts b/lib/vocabularies/applicator/patternProperties.ts index 73ec882e7..8ee867969 100644 --- a/lib/vocabularies/applicator/patternProperties.ts +++ b/lib/vocabularies/applicator/patternProperties.ts @@ -57,7 +57,6 @@ const def: CodeKeywordDefinition = { schemaProp: pat, dataProp: key, dataPropType: Type.Str, - strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/applicator/properties.ts b/lib/vocabularies/applicator/properties.ts index 4bab7b69c..5ab96acd0 100644 --- a/lib/vocabularies/applicator/properties.ts +++ b/lib/vocabularies/applicator/properties.ts @@ -43,7 +43,6 @@ const def: CodeKeywordDefinition = { keyword: "properties", schemaProp: prop, dataProp: prop, - strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/applicator/propertyNames.ts b/lib/vocabularies/applicator/propertyNames.ts index 5df331274..046361635 100644 --- a/lib/vocabularies/applicator/propertyNames.ts +++ b/lib/vocabularies/applicator/propertyNames.ts @@ -34,7 +34,6 @@ const def: CodeKeywordDefinition = { dataTypes: ["string"], propertyName: key, compositeRule: true, - strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index 3c4d3a1bd..b02cd3e7b 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -105,7 +105,6 @@ export function validateArray(cxt: KeywordCxt): Name { keyword, dataProp: i, dataPropType: Type.Num, - strictSchema: it.strictSchema, }, valid ) diff --git a/lib/vocabularies/core/ref.ts b/lib/vocabularies/core/ref.ts index b058607f5..077e4491b 100644 --- a/lib/vocabularies/core/ref.ts +++ b/lib/vocabularies/core/ref.ts @@ -41,7 +41,6 @@ const def: CodeKeywordDefinition = { const schCxt = cxt.subschema( { schema: sch, - strictSchema: true, dataTypes: [], schemaPath: nil, topSchemaRef: schName, diff --git a/lib/vocabularies/jtd/discriminator.ts b/lib/vocabularies/jtd/discriminator.ts index 92dde8278..de086b89b 100644 --- a/lib/vocabularies/jtd/discriminator.ts +++ b/lib/vocabularies/jtd/discriminator.ts @@ -10,7 +10,7 @@ const def: CodeKeywordDefinition = { implements: ["mapping"], code(cxt: KeywordCxt) { checkMetadata(cxt) - const {gen, data, schema, parentSchema, it} = cxt + const {gen, data, schema, parentSchema} = cxt const [valid, cond] = checkNullableObject(cxt, data) gen.if(cond, () => { @@ -32,7 +32,6 @@ const def: CodeKeywordDefinition = { { keyword: "mapping", schemaProp, - strictSchema: it.strictSchema, jtdDiscriminator: schema, }, _valid diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 92f943c5c..8b38b6083 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -77,7 +77,6 @@ export function validateProperties(cxt: KeywordCxt): void { keyword, schemaProp: prop, dataProp: prop, - strictSchema: it.strictSchema, }, _valid ) diff --git a/lib/vocabularies/jtd/ref.ts b/lib/vocabularies/jtd/ref.ts index 929a3cb36..da6956003 100644 --- a/lib/vocabularies/jtd/ref.ts +++ b/lib/vocabularies/jtd/ref.ts @@ -49,7 +49,6 @@ const def: CodeKeywordDefinition = { cxt.subschema( { schema, - strictSchema: true, dataTypes: [], schemaPath: nil, topSchemaRef: schName, diff --git a/lib/vocabularies/jtd/values.ts b/lib/vocabularies/jtd/values.ts index 96488c4b2..d36c666dc 100644 --- a/lib/vocabularies/jtd/values.ts +++ b/lib/vocabularies/jtd/values.ts @@ -35,7 +35,6 @@ const def: CodeKeywordDefinition = { keyword: "values", dataProp: key, dataPropType: Type.Str, - strictSchema: it.strictSchema, }, _valid ) diff --git a/lib/vocabularies/unevaluated/unevaluatedProperties.ts b/lib/vocabularies/unevaluated/unevaluatedProperties.ts index d716e53d3..655e645bc 100644 --- a/lib/vocabularies/unevaluated/unevaluatedProperties.ts +++ b/lib/vocabularies/unevaluated/unevaluatedProperties.ts @@ -62,7 +62,6 @@ const def: CodeKeywordDefinition = { keyword: "unevaluatedProperties", dataProp: key, dataPropType: Type.Str, - strictSchema: it.strictSchema, }, valid ) From 5ddef2e4c20da50e3a5aaea15987be5dc0d374bb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 8 Feb 2021 20:04:11 +0000 Subject: [PATCH 24/29] JTD docs --- README.md | 20 +++++------ docs/json-type-definition.md | 65 +++++++++++++++++++++++++++++++----- docs/strict-mode.md | 52 +++++++++++++++++++---------- 3 files changed, 101 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 8b3ebf509..41a995cc7 100644 --- a/README.md +++ b/README.md @@ -357,23 +357,23 @@ This section compares their pros/cons to help decide which specification fits yo ### JSON Schema - Pros + - Wide specification adoption. + - Used as part of OpenAPI specification. - Support of complex validation scenarios: - untagged unions and boolean logic - conditional schemas and dependencies - - restrictions on number ranges and string, array and object sizes + - restrictions on the number ranges and the size of strings, arrays and objects - semantic validation with formats, patterns and content keywords - distribute strict record definitions across multiple schemas (with unevaluatedProperties) - - Wide specification adoption. - - Used as part of OpenAPI specification. - Can be effectively used for validation of any JavaScript objects and configuration files. - Cons - - Defines the collection of restrictions on your data, rather than the shape of the data. + - Defines the collection of restrictions on the data, rather than the shape of the data. - No standard support for tagged unions. - - Complex, error prone and often confusing for the new users (Ajv has [strict mode](./docs/strict-mode) to compensate for it, but it is not cross-platform). - - Some parts of specification are difficult to implement, creating the risk of divergence of implementations: + - Complex and error prone for the new users (Ajv has [strict mode](./docs/strict-mode) to compensate for it, but it is not cross-platform). + - Some parts of specification are difficult to implement, creating the risk of implementations divergence: - reference resolution model - unevaluatedProperties/unevaluatedItems - - recursive references + - dynamic recursive references - Internet draft status (rather than RFC) See [JSON Schema](./docs/json-schema.md) for the list of defined keywords. @@ -382,20 +382,20 @@ See [JSON Schema](./docs/json-schema.md) for the list of defined keywords. - Pros: - Aligned with type systems of many languages - can be used to generate type definitions and efficient parsers and serializers to/from these types. - - Extremely simple, enforcing best practices for cross-platform JSON API modelling. + - Very simple, enforcing the best practices for cross-platform JSON API modelling. - Simple to implement, ensuring consistency across implementations. - Defines the shape of JSON data via strictly defined schema forms (rather than the collection of restrictions). - Effective support for tagged unions. - Designed to protect against user mistakes. - Approved as [RFC8927](https://datatracker.ietf.org/doc/rfc8927/) - Cons: - - Limited, compared with JSON Schema - no untagged unions\*, conditionals, references between schemas\*\*, etc. + - Limited, compared with JSON Schema - no support for untagged unions\*, conditionals, references between different schema files\*\*, etc. - No meta-schema in the specification\*. - Brand new - limited industry adoption (as of January 2021). \* Ajv defines meta-schema for JTD schemas and non-standard keyword "union" that can be used inside "metadata" object. -\*\* You can still combine schemas in JavaScript code. +\*\* You can still combine schemas from multiple files in the application code. See [JSON Type Definition](./docs/json-type-definition.md) for the list of defined schema forms. diff --git a/docs/json-type-definition.md b/docs/json-type-definition.md index dfa0257cd..2abb1f3d0 100644 --- a/docs/json-type-definition.md +++ b/docs/json-type-definition.md @@ -1,6 +1,24 @@ # JSON Type Definition -This document informally describes JSON Type Definitions specification to help Ajv users to start using it. For formal definition please refer to [RFC8927](https://datatracker.ietf.org/doc/rfc8927/). Please report any contradictions in this document with the specification. +This document informally describes JSON Type Definition (JTD) specification to help Ajv users to start using it. For formal definition please refer to [RFC8927](https://datatracker.ietf.org/doc/rfc8927/). Please report any contradictions in this document with the specification. + +## Contents + +- [JTD schema forms](#jtd-schema-forms): + - [type](#type-schema-form) (for primitive values) + - [enum](#enum-schema-form) + - [elements](#elements-schema-form) (for arrays) + - [properties](#properties-schema-form) (for records) + - [discriminator](#discriminator-schema-form) (for tagged union of records) + - [values](#values-schema-form) (for dictionary) + - [ref](#ref-schema-form) (to reference a schema in definitions) + - [empty](#empty-schema-form) (for any data) +- [Extending JTD](#extending-jtd) + - [metadata](#metadata-schema-member) + - [union](#union-keyword) + - [user-defined keywords](#user-defined-keywords) + +## JTD schema forms JTD specification defines 8 different forms that the schema for JSON can take for one of most widely used data types in JSON messages (API requests and responses). @@ -13,7 +31,7 @@ All forms require that: Root schema can have member `definitions` that has a dictionary of schemas that can be references from any other schemas using form `ref` -## Type schema form +### Type schema form This form defines a primitive value. @@ -45,7 +63,7 @@ Unlike JSON Schema, JTD does not allow defining values that can take one of seve } ``` -## Enum schema form +### Enum schema form This form defines a string that can take one of the values from the list (the values in the list must be unique). @@ -61,7 +79,7 @@ Unlike JSON Schema, JTD does not allow defining `enum` with values of any other } ``` -## Elements schema form +### Elements schema form This form defines a homogenous array of any size (possibly empty) with the elements that satisfy a given schema. @@ -85,7 +103,7 @@ Valid data: `[]`, `["foo"]`, `["foo", "bar"]` Invalid data: `["foo", 1]`, any type other than array -## Properties schema form +### Properties schema form This form defines record (JSON object) that has defined required and optional properties. @@ -144,7 +162,7 @@ Invalid data: `{}`, `{foo: 1}`, `{foo: "bar", bar: "3"}`, any type other than ob } ``` -## Discriminator schema form +### Discriminator schema form This form defines discriminated (tagged) union of different record types. @@ -202,7 +220,7 @@ Invalid data: `{}`, `{foo: "1"}`, `{version: 1, foo: "1"}`, any type other than } ``` -## Values schema form +### Values schema form This form defines a homogenous dictionary where the values of members satisfy a given schema. @@ -226,7 +244,7 @@ Valid data: `{}`, `{"foo": 1}`, `{"foo": 1, "bar": 2}` Invalid data: `{"foo": "bar"}`, any type other than object -## Ref schema form +### Ref schema form This form defines a reference to the schema that is present in the corresponding key in the `definitions` member of the root schema. @@ -281,6 +299,35 @@ Unlike JSON Schema, JTD does not allow to reference: } ``` -## Empty schema form +### Empty schema form Empty JTD schema defines the data instance that can be of any type, including JSON `null` (even if `nullable` member is not present). It cannot have any member other than `nullable` and `metadata`. + +## Extending JTD + +### Metadata schema member + +Each schema form may have an optional member `metadata` that JTD reserves for implementation/application specific extensions. Ajv uses this member as a location where any non-standard keywords can be used, such as: +- `union` keyword included in Ajv +- any user-defined keywords, for example keywords defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package +- JSON Schema keywords, as long as their names are different from standard JTD keywords. It can be used to enable a gradual migration from JSON Schema to JTD, should it be required. + +**Please note**: Ajv-specific extension to JTD are likely to be unsupported by other tools, so while it may simplify adoption, it undermines the cross-platform objective of using JTD. While it is ok to put some human readable information in `metadata` member, it is recommended not to add any validation logic there (even if it is supported by Ajv). + +Additional restrictions that Ajv enforces on `metadata` schema member: +- you cannot use standard JTD keywords there. While strictly speaking it is allowed by the specification, these keywords should be ignored inside `metadata` - the general approach of Ajv is to avoid anything that is ignored. +- you need to define all members used in `metadata` as keywords. If they are no-op it can be done with `ajv.addKeyword("my-metadata-keyword")`. This restriction can be removed by disabling [strict mode](https://github.com/ajv-validator/ajv/blob/master/docs/strict-mode.md), without affecting the strictness of JTD - unknown keywords would still be prohibited in the schema itself. + +### Union keyword + +Ajv defines `union` keyword that is used in the schema that validates JTD schemas ([meta-schema](../lib/refs/jtd-schema.ts)). + +This keyword can be used only inside `metadata` schema member. + +**Please note**: This keyword is non-standard and it is not supported in other JTD tools, so it is recommended NOT to use this keyword in schemas for your data if you want them to be cross-platform. + +### User-defined keywords + +Any user-defined keywords that can be used in JSON Schema schemas can also be used in JTD schemas, including the keywords in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. + +**Please note**: It is strongly recommended to only use it to simplify migration from JSON Schema to JTD and not to use non-standard keywords in the new schemas, as these keywords are not supported by any other tools. \ No newline at end of file diff --git a/docs/strict-mode.md b/docs/strict-mode.md index e098064ea..feda8bd07 100644 --- a/docs/strict-mode.md +++ b/docs/strict-mode.md @@ -1,24 +1,42 @@ -## Strict mode +# Strict mode -Strict mode intends to prevent any unexpected behaviours or silently ignored mistakes in user schemas. It does not change any validation results compared with JSON Schema specification, but it makes some schemas invalid and throws exception or logs warning (with `strict: "log"` option) in case any restriction is violated. +Strict mode intends to prevent any unexpected behaviours or silently ignored mistakes in user schemas. It does not change any validation results compared with the specification, but it makes some schemas invalid and throws exception or logs warning (with `strict: "log"` option) in case any restriction is violated. To disable all strict mode restrictions use option `strict: false`. Some of the restrictions can be changed with their own options -- [Prohibit ignored keywords](#prohibit-ignored-keywords) - - unknown keywords - - ignored "additionalItems" keyword - - ignored "if", "then", "else" keywords - - ignored "contains", "maxContains" and "minContains" keywords - - unknown formats - - ignored defaults -- [Prevent unexpected validation](#prevent-unexpected-validation) - - overlap between "properties" and "patternProperties" keywords (also `allowMatchingProperties` option) - - unconstrained tuples (also `strictTuples` option) -- [Strict types](#strict-types) (also `strictTypes` option) - - union types (also `allowUnionTypes` option) - - contradictory types - - require applicable types -- [Strict number validation](#strict-number-validation) +- [JSON Type Definition schemas](#json-type-definition-schemas) +- [JSON Schema schemas](#json-schema-schemas) + - [Prohibit ignored keywords](#prohibit-ignored-keywords) + - unknown keywords + - ignored "additionalItems" keyword + - ignored "if", "then", "else" keywords + - ignored "contains", "maxContains" and "minContains" keywords + - unknown formats + - ignored defaults + - [Prevent unexpected validation](#prevent-unexpected-validation) + - overlap between "properties" and "patternProperties" keywords (also `allowMatchingProperties` option) + - unconstrained tuples (also `strictTuples` option) + - [Strict types](#strict-types) (also `strictTypes` option) + - union types (also `allowUnionTypes` option) + - contradictory types + - require applicable types + - [Strict number validation](#strict-number-validation) + +## JSON Type Definition schemas + +JTD specification is strict - whether Ajv strict mode is enabled or not it will not allow schemas with ignored or ambiguous elements, including: +- unknown schema keywords +- combining multiple schema forms in one schema +- defining the same property as both required and optional +- re-defining discriminator tag inside properties, even if the definition is non-contradictory + +See [JSON Type Definition](./json-type-definition.md) for informal and [RFC8927](https://datatracker.ietf.org/doc/rfc8927/) for formal specification descriptions. + +The only change that strict mode introduces to JTD schemas, without changing their syntax or semantics, is the requirement that all members that are present in optional `metadata` members are defined as Ajv keywords. This restriction can be disabled with `strict: false` option, without any impact to other JTD features. + +## JSON Schema schemas + +JSON Schema specification is very permissive and allows many elements in the schema to be quietly ignored or be ambiguous. It is recommended to use JSON Schema with strict mode. ### Prohibit ignored keywords From 4a9084a2c312d20435f4f4215b8b0c9889b6b708 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 9 Feb 2021 20:24:05 +0000 Subject: [PATCH 25/29] jtd error format --- docs/json-type-definition.md | 4 +++- docs/strict-mode.md | 1 + lib/compile/errors.ts | 25 ++++++++++++++++++------- lib/compile/index.ts | 2 +- spec/jtd-schema.spec.ts | 23 +++++++++++++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/docs/json-type-definition.md b/docs/json-type-definition.md index 2abb1f3d0..29f7c2f77 100644 --- a/docs/json-type-definition.md +++ b/docs/json-type-definition.md @@ -308,6 +308,7 @@ Empty JTD schema defines the data instance that can be of any type, including JS ### Metadata schema member Each schema form may have an optional member `metadata` that JTD reserves for implementation/application specific extensions. Ajv uses this member as a location where any non-standard keywords can be used, such as: + - `union` keyword included in Ajv - any user-defined keywords, for example keywords defined in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package - JSON Schema keywords, as long as their names are different from standard JTD keywords. It can be used to enable a gradual migration from JSON Schema to JTD, should it be required. @@ -315,6 +316,7 @@ Each schema form may have an optional member `metadata` that JTD reserves for im **Please note**: Ajv-specific extension to JTD are likely to be unsupported by other tools, so while it may simplify adoption, it undermines the cross-platform objective of using JTD. While it is ok to put some human readable information in `metadata` member, it is recommended not to add any validation logic there (even if it is supported by Ajv). Additional restrictions that Ajv enforces on `metadata` schema member: + - you cannot use standard JTD keywords there. While strictly speaking it is allowed by the specification, these keywords should be ignored inside `metadata` - the general approach of Ajv is to avoid anything that is ignored. - you need to define all members used in `metadata` as keywords. If they are no-op it can be done with `ajv.addKeyword("my-metadata-keyword")`. This restriction can be removed by disabling [strict mode](https://github.com/ajv-validator/ajv/blob/master/docs/strict-mode.md), without affecting the strictness of JTD - unknown keywords would still be prohibited in the schema itself. @@ -330,4 +332,4 @@ This keyword can be used only inside `metadata` schema member. Any user-defined keywords that can be used in JSON Schema schemas can also be used in JTD schemas, including the keywords in [ajv-keywords](https://github.com/ajv-validator/ajv-keywords) package. -**Please note**: It is strongly recommended to only use it to simplify migration from JSON Schema to JTD and not to use non-standard keywords in the new schemas, as these keywords are not supported by any other tools. \ No newline at end of file +**Please note**: It is strongly recommended to only use it to simplify migration from JSON Schema to JTD and not to use non-standard keywords in the new schemas, as these keywords are not supported by any other tools. diff --git a/docs/strict-mode.md b/docs/strict-mode.md index feda8bd07..bda1912fb 100644 --- a/docs/strict-mode.md +++ b/docs/strict-mode.md @@ -25,6 +25,7 @@ To disable all strict mode restrictions use option `strict: false`. Some of the ## JSON Type Definition schemas JTD specification is strict - whether Ajv strict mode is enabled or not it will not allow schemas with ignored or ambiguous elements, including: + - unknown schema keywords - combining multiple schema forms in one schema - defining the same property as both required and optional diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 86565bf8a..e5214d12f 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -97,22 +97,33 @@ function returnErrors(it: SchemaCxt, errs: Code): void { const E = { keyword: new Name("keyword"), - schemaPath: new Name("schemaPath"), + schemaPath: new Name("schemaPath"), // also used in JTD errors params: new Name("params"), propertyName: new Name("propertyName"), message: new Name("message"), schema: new Name("schema"), parentSchema: new Name("parentSchema"), + // JTD error properties + instancePath: new Name("instancePath"), } function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { - const { - keyword, - data, - schemaValue, - it: {gen, createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, - } = cxt + const {createErrors, opts} = cxt.it if (createErrors === false) return _`{}` + return opts.jtd ? jtdErrorObject(cxt) : ajvErrorObject(cxt, error) +} + +function jtdErrorObject({gen, keyword, it}: KeywordErrorCxt): Code { + const {errorPath, errSchemaPath} = it + return gen.object( + [E.instancePath, strConcat(N.dataPath, errorPath)], + [E.schemaPath, str`${errSchemaPath}/${keyword}`] + ) +} + +function ajvErrorObject(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { + const {keyword, data, schemaValue, it} = cxt + const {gen, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts} = it const {params, message} = error const keyValues: [Name, SafeExpr | string][] = [ [E.keyword, keyword], diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 79c9101bb..9102cc570 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -139,7 +139,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { rootId, baseId: sch.baseId || rootId, schemaPath: nil, - errSchemaPath: "#", + errSchemaPath: this.opts.jtd ? "" : "#", errorPath: _`""`, opts: this.opts, self: this, diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 45a0ca5d0..ba1c0279b 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -6,6 +6,7 @@ import {withStandalone} from "./ajv_standalone" import jtdValidationTests = require("./json-typedef-spec/tests/validation.json") import jtdInvalidSchemasTests = require("./json-typedef-spec/tests/invalid_schemas.json") import assert = require("assert") +// import AjvPack from "../dist/standalone/instance" interface TestCase { schema: SchemaObject @@ -18,6 +19,11 @@ interface TestCaseError { schemaPath: string[] } +// interface JTDError { +// instancePath: string +// schemaPath: string +// } + // const ONLY: RegExp[] = [ // "empty", // "ref", @@ -54,9 +60,26 @@ describe("JSON Type Definition", () => { // console.log(ajv.compile(schema).toString()) // console.log(ajv.validate(schema, instance), ajv.errors) assert.strictEqual(ajv.validate(schema, instance), valid) + // const opts = ajv instanceof AjvPack ? ajv.ajv.opts : ajv.opts + // if (opts.allErrors) { + // assert.deepStrictEqual(ajv.errors, valid ? null : convertErrors(errors)) + // } })) ) } + + // function convertErrors(errors: TestCaseError[]): JTDError[] { + // return errors.map((e) => + // ({ + // instancePath: jsonPointer(e.instancePath), + // schemaPath: jsonPointer(e.schemaPath) + // }) + // ) + // } + + // function jsonPointer(error: string[]): string { + // return error.map((s) => `/${s}`).join("") + // } }) describe("invalid schemas", () => { From 7e1711e33996484f134ce32d5ec4c8c7c46e508f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:45:50 +0000 Subject: [PATCH 26/29] JTD options, support inlineRefs option, fix removeAdditional option --- docs/api.md | 45 ++++++++++++++++++++---------- docs/json-type-definition.md | 10 +++++++ lib/compile/errors.ts | 26 +++++++++-------- lib/core.ts | 3 +- lib/jtd.ts | 33 ++++++++++++++++++++-- lib/vocabularies/jtd/properties.ts | 13 +++++---- lib/vocabularies/jtd/ref.ts | 2 +- spec/jtd-schema.spec.ts | 1 + 8 files changed, 98 insertions(+), 35 deletions(-) diff --git a/docs/api.md b/docs/api.md index b206c8951..0986c0556 100644 --- a/docs/api.md +++ b/docs/api.md @@ -251,36 +251,37 @@ Option defaults: const defaultOptions = { // strict mode options (NEW) strict: true, - strictTypes: "log", - strictTuples: "log", - allowUnionTypes: false, - allowMatchingProperties: false, - validateFormats: true, + strictTypes: "log", // * + strictTuples: "log", // * + allowUnionTypes: false, // * + allowMatchingProperties: false, // * + validateFormats: true, // * // validation and reporting options: - $data: false, + $data: false, // * allErrors: false, - verbose: false, - $comment: false, + verbose: false, // * + $comment: false, // * formats: {}, keywords: {}, schemas: {}, logger: undefined, - loadSchema: undefined, // function(uri: string): Promise {} + loadSchema: undefined, // *, function(uri: string): Promise {} // options to modify validated data: removeAdditional: false, - useDefaults: false, - coerceTypes: false, + useDefaults: false, // * + coerceTypes: false, // * // advanced options: meta: true, validateSchema: true, addUsedSchema: true, inlineRefs: true, passContext: false, - loopRequired: Infinity, + loopRequired: Infinity, // * loopEnum: Infinity, // NEW ownProperties: false, - multipleOfPrecision: undefined, - messages: true, + multipleOfPrecision: undefined, // * + messages: true, // false with JTD + ajvErrors: false // only with JTD code: { // NEW es5: false, @@ -292,6 +293,8 @@ const defaultOptions = { } ``` +\* these options are not supported with JSON Type Definition schemas + #### Strict mode options (NEW in v7) - _strict_: By default Ajv executes in strict mode, that is designed to prevent any unexpected behaviours or silently ignored mistakes in schemas (see [Strict Mode](./strict-mode.md) for more details). It does not change any validation results, but it makes some schemas invalid that would be otherwise valid according to JSON Schema specification. Option values: @@ -363,6 +366,7 @@ const defaultOptions = { - _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/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 arithmetic deviations). - _messages_: Include human-readable messages in errors. `true` by default. `false` can be passed when messages are generated outside of Ajv code (e.g. with [ajv-i18n](https://github.com/ajv-validator/ajv-i18n)). +- _ajvErrors_: this option is only supported with JTD schemas to generate error objects with the properties described in the first part of [Validation errors](#validation-errors) section, otherwise JTD errors are generated when JTD schemas are used (see the second part of [the same section](#validation-errors)). - _code_ (new in v7): code generation options: ```typescript @@ -393,7 +397,7 @@ In case of validation failure, Ajv assigns the array of errors to `errors` prope ### Error objects -Each error is an object with the following properties: +Each error reported when validating against JSON Schema (also when validating against JTD schema with option `ajvErrors`) is an object with the following properties: ```typescript interface ErrorObject { @@ -413,6 +417,17 @@ interface ErrorObject { } ``` +[JTD specification](./json-type-definition.md) defines strict format for validation errors, where each error is an object with the following properties: + +```typescript +interface JTDErrorObject { + instancePath: string // JSON Pointer to the location in the data instance + schemaPath: string // JSON Pointer to the location in the schema +} +``` + +This error format is used when using JTD schemas. To simplify usage, you may still generate Ajv error objects using `ajvErrors` option. You can also add a human-readable error message to error objects using option `messages`. + ### Error parameters Properties of `params` object in errors depend on the keyword that failed validation. diff --git a/docs/json-type-definition.md b/docs/json-type-definition.md index 29f7c2f77..4a8dc0015 100644 --- a/docs/json-type-definition.md +++ b/docs/json-type-definition.md @@ -2,6 +2,15 @@ This document informally describes JSON Type Definition (JTD) specification to help Ajv users to start using it. For formal definition please refer to [RFC8927](https://datatracker.ietf.org/doc/rfc8927/). Please report any contradictions in this document with the specification. +To use JTD schemas you need to import a different Ajv class: + +```javascript +const AjvJTD = require("ajv/dist/jtd").default +// or in TypeScript: +// import Ajv from "ajv/dist/jtd" +const ajv = new AjvJTD() +``` + ## Contents - [JTD schema forms](#jtd-schema-forms): @@ -17,6 +26,7 @@ This document informally describes JSON Type Definition (JTD) specification to h - [metadata](#metadata-schema-member) - [union](#union-keyword) - [user-defined keywords](#user-defined-keywords) +- [Validation errors](#validation-errors) ## JTD schema forms diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index e5214d12f..47f51c35d 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -110,20 +110,25 @@ const E = { function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { const {createErrors, opts} = cxt.it if (createErrors === false) return _`{}` - return opts.jtd ? jtdErrorObject(cxt) : ajvErrorObject(cxt, error) + return opts.jtd && !opts.ajvErrors ? jtdErrorObject(cxt, error) : ajvErrorObject(cxt, error) } -function jtdErrorObject({gen, keyword, it}: KeywordErrorCxt): Code { - const {errorPath, errSchemaPath} = it - return gen.object( +function jtdErrorObject(cxt: KeywordErrorCxt, {message}: KeywordErrorDefinition): Code { + const {gen, keyword, it} = cxt + const {errorPath, errSchemaPath, opts} = it + const keyValues: [Name, SafeExpr | string][] = [ [E.instancePath, strConcat(N.dataPath, errorPath)], - [E.schemaPath, str`${errSchemaPath}/${keyword}`] - ) + [E.schemaPath, str`${errSchemaPath}/${keyword}`], + ] + if (opts.messages) { + keyValues.push([E.message, typeof message == "function" ? message(cxt) : message]) + } + return gen.object(...keyValues) } function ajvErrorObject(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { - const {keyword, data, schemaValue, it} = cxt - const {gen, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts} = it + const {gen, keyword, data, schemaValue, it} = cxt + const {topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts} = it const {params, message} = error const keyValues: [Name, SafeExpr | string][] = [ [E.keyword, keyword], @@ -132,9 +137,8 @@ function ajvErrorObject(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Co [E.params, typeof params == "function" ? params(cxt) : params || _`{}`], ] if (propertyName) keyValues.push([E.propertyName, propertyName]) - if (opts.messages !== false) { - const msg = typeof message == "function" ? message(cxt) : message - keyValues.push([E.message, msg]) + if (opts.messages) { + keyValues.push([E.message, typeof message == "function" ? message(cxt) : message]) } if (opts.verbose) { keyValues.push( diff --git a/lib/core.ts b/lib/core.ts index b6577c25b..d0085608b 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -77,7 +77,7 @@ const EXT_SCOPE_NAMES = new Set([ export type Options = CurrentOptions & DeprecatedOptions -interface CurrentOptions { +export interface CurrentOptions { // strict mode options (NEW) strict?: boolean | "log" strictTypes?: boolean | "log" @@ -118,6 +118,7 @@ interface CurrentOptions { multipleOfPrecision?: number messages?: boolean code?: CodeOptions // NEW + ajvErrors?: boolean } export interface CodeOptions { diff --git a/lib/jtd.ts b/lib/jtd.ts index dd15d07aa..a31980e11 100644 --- a/lib/jtd.ts +++ b/lib/jtd.ts @@ -27,7 +27,7 @@ export {KeywordCxt} export {_, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions} from "./compile/codegen" import type {AnySchemaObject} from "./types" -import AjvCore, {Options} from "./core" +import AjvCore, {CurrentOptions} from "./core" import jtdVocabulary from "./vocabularies/jtd" import jtdMetaSchema from "./refs/jtd-schema" @@ -35,11 +35,40 @@ import jtdMetaSchema from "./refs/jtd-schema" const META_SCHEMA_ID = "JTD-meta-schema" +export type JTDOptions = CurrentOptions & { + // strict mode options not supported with JTD: + strictTypes?: never + strictTuples?: never + allowMatchingProperties?: never + allowUnionTypes?: never + validateFormats?: never + // validation and reporting options not supported with JTD: + $data?: never + verbose?: never + $comment?: never + formats?: never + loadSchema?: never + // options to modify validated data: + useDefaults?: never + coerceTypes?: never + // advanced options: + next?: never + unevaluated?: never + dynamicRef?: never + meta?: boolean + defaultMeta?: never + inlineRefs?: boolean + loopRequired?: never + multipleOfPrecision?: never + ajvErrors?: boolean +} + export default class Ajv extends AjvCore { - constructor(opts: Options = {}) { + constructor(opts: JTDOptions = {}) { super({ ...opts, jtd: true, + messages: opts.messages ?? false, }) } diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index 8b38b6083..aa21f82ae 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -92,11 +92,14 @@ export function validateProperties(cxt: KeywordCxt): void { const extra = addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp) gen.if(extra, () => { - if (it.opts.removeAdditional) gen.code(_`delete ${data}[${key}]`) - // cxt.setParams({additionalProperty: key}) - cxt.error() - gen.assign(valid, false) - if (!it.opts.allErrors) gen.break() + if (it.opts.removeAdditional) { + gen.code(_`delete ${data}[${key}]`) + } else { + // cxt.setParams({additionalProperty: key}) + cxt.error() + gen.assign(valid, false) + if (!it.opts.allErrors) gen.break() + } }) }) } diff --git a/lib/vocabularies/jtd/ref.ts b/lib/vocabularies/jtd/ref.ts index da6956003..e354bf969 100644 --- a/lib/vocabularies/jtd/ref.ts +++ b/lib/vocabularies/jtd/ref.ts @@ -29,7 +29,7 @@ const def: CodeKeywordDefinition = { function validateJtdRef(): void { const refSchema = (root.schema as AnySchemaObject).definitions?.[ref] if (!refSchema) throw new MissingRefError("", ref, `No definition ${ref}`) - if (hasRef(refSchema)) callValidate(refSchema) + if (hasRef(refSchema) || !it.opts.inlineRefs) callValidate(refSchema) else inlineRefSchema(refSchema) } diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index ba1c0279b..248584609 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -46,6 +46,7 @@ describe("JSON Type Definition", () => { before(() => { ajvs = getAjvInstances(_AjvJTD, { allErrors: true, + inlineRefs: false, code: {es5: true, lines: true, optimize: false}, }) ajvs.forEach((ajv) => (ajv.opts.code.source = true)) From d6acee4bac033b3389393b71785afbce275fd632 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 10 Feb 2021 11:54:07 +0000 Subject: [PATCH 27/29] add optional instancePath property to error object --- lib/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/types/index.ts b/lib/types/index.ts index e91f122b9..5fc9b6d2a 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -76,6 +76,7 @@ export type AnyValidateFunction = ValidateFunction | AsyncValidateFu export interface ErrorObject, S = unknown> { keyword: K dataPath: string + instancePath?: string schemaPath: string params: P // Added to validation errors of "propertyNames" keyword schema From 9e5f2470e5c81a4de4bd2c6eb885eaac93914fba Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 10 Feb 2021 21:59:19 +0000 Subject: [PATCH 28/29] JTD: improve error reporting, remove extra error for properties --- lib/compile/context.ts | 10 ++-------- lib/compile/errors.ts | 9 ++++++--- lib/vocabularies/jtd/properties.ts | 21 ++++++++++++++------- lib/vocabularies/jtd/ref.ts | 4 ++-- spec/jtd-schema.spec.ts | 10 ++++------ 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/compile/context.ts b/lib/compile/context.ts index 2830b0692..d2472eedf 100644 --- a/lib/compile/context.ts +++ b/lib/compile/context.ts @@ -8,13 +8,7 @@ import {SchemaCxt, SchemaObjCxt} from "./index" import {JSONType} from "./rules" import {checkDataTypes, DataType} from "./validate/dataType" import {schemaRefOrVal, unescapeJsonPointer, mergeEvaluated} from "./util" -import { - reportError, - reportExtraError, - resetErrorsCount, - keywordError, - keyword$DataError, -} from "./errors" +import {reportError, reportExtraError, resetErrorsCount, keyword$DataError} from "./errors" import {CodeGen, _, nil, or, not, getProperty, Code, Name} from "./codegen" import N from "./names" import {applySubschema, SubschemaArgs} from "./subschema" @@ -102,7 +96,7 @@ export default class KeywordCxt implements KeywordErrorCxt { } error(append?: true): void { - ;(append ? reportExtraError : reportError)(this, this.def.error || keywordError) + ;(append ? reportExtraError : reportError)(this, this.def.error) } $dataError(): void { diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 47f51c35d..e1a497c89 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -17,7 +17,7 @@ export const keyword$DataError: KeywordErrorDefinition = { export function reportError( cxt: KeywordErrorCxt, - error: KeywordErrorDefinition, + error: KeywordErrorDefinition = keywordError, overrideAllErrors?: boolean ): void { const {it} = cxt @@ -30,7 +30,10 @@ export function reportError( } } -export function reportExtraError(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): void { +export function reportExtraError( + cxt: KeywordErrorCxt, + error: KeywordErrorDefinition = keywordError +): void { const {it} = cxt const {gen, compositeRule, allErrors} = it const errObj = errorObjectCode(cxt, error) @@ -110,7 +113,7 @@ const E = { function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { const {createErrors, opts} = cxt.it if (createErrors === false) return _`{}` - return opts.jtd && !opts.ajvErrors ? jtdErrorObject(cxt, error) : ajvErrorObject(cxt, error) + return (opts.jtd && !opts.ajvErrors ? jtdErrorObject : ajvErrorObject)(cxt, error) } function jtdErrorObject(cxt: KeywordErrorCxt, {message}: KeywordErrorDefinition): Code { diff --git a/lib/vocabularies/jtd/properties.ts b/lib/vocabularies/jtd/properties.ts index aa21f82ae..5d881d6ac 100644 --- a/lib/vocabularies/jtd/properties.ts +++ b/lib/vocabularies/jtd/properties.ts @@ -60,18 +60,27 @@ export function validateProperties(cxt: KeywordCxt): void { } function validateProps(props: string[], keyword: string, required?: boolean): void { + const _valid = gen.var("valid") for (const prop of props) { gen.if( propertyInData(data, prop, it.opts.ownProperties), - () => gen.assign(valid, and(valid, applyPropertySchema(prop, keyword))), - required ? () => gen.assign(valid, false) : undefined + () => applyPropertySchema(prop, keyword, _valid), + missingProperty ) - cxt.ok(valid) + cxt.ok(_valid) + } + + function missingProperty(): void { + if (required) { + gen.assign(_valid, false) + cxt.error() + } else { + gen.assign(_valid, true) + } } } - function applyPropertySchema(prop: string, keyword: string): Name { - const _valid = gen.name("valid") + function applyPropertySchema(prop: string, keyword: string, _valid: Name): void { cxt.subschema( { keyword, @@ -80,7 +89,6 @@ export function validateProperties(cxt: KeywordCxt): void { }, _valid ) - return _valid } function validateAdditional(): void { @@ -97,7 +105,6 @@ export function validateProperties(cxt: KeywordCxt): void { } else { // cxt.setParams({additionalProperty: key}) cxt.error() - gen.assign(valid, false) if (!it.opts.allErrors) gen.break() } }) diff --git a/lib/vocabularies/jtd/ref.ts b/lib/vocabularies/jtd/ref.ts index e354bf969..5bec32d18 100644 --- a/lib/vocabularies/jtd/ref.ts +++ b/lib/vocabularies/jtd/ref.ts @@ -24,7 +24,7 @@ const def: CodeKeywordDefinition = { gen.var(valid, false) validateJtdRef() } - cxt.pass(valid) + cxt.ok(valid) function validateJtdRef(): void { const refSchema = (root.schema as AnySchemaObject).definitions?.[ref] @@ -52,7 +52,7 @@ const def: CodeKeywordDefinition = { dataTypes: [], schemaPath: nil, topSchemaRef: schName, - errSchemaPath: `#/definitions/${ref}`, + errSchemaPath: `/definitions/${ref}`, }, valid ) diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index 248584609..3bc386f18 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -70,12 +70,10 @@ describe("JSON Type Definition", () => { } // function convertErrors(errors: TestCaseError[]): JTDError[] { - // return errors.map((e) => - // ({ - // instancePath: jsonPointer(e.instancePath), - // schemaPath: jsonPointer(e.schemaPath) - // }) - // ) + // return errors.map((e) => ({ + // instancePath: jsonPointer(e.instancePath), + // schemaPath: jsonPointer(e.schemaPath), + // })) // } // function jsonPointer(error: string[]): string { From 078d6a8a9ad3dfcd5ef840d25a051b8b3ecd43be Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 10 Feb 2021 22:05:49 +0000 Subject: [PATCH 29/29] note on JTD error objects --- docs/api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api.md b/docs/api.md index 0986c0556..f3a274975 100644 --- a/docs/api.md +++ b/docs/api.md @@ -428,6 +428,8 @@ interface JTDErrorObject { This error format is used when using JTD schemas. To simplify usage, you may still generate Ajv error objects using `ajvErrors` option. You can also add a human-readable error message to error objects using option `messages`. +**Please note**: Ajv is not fully consistent with JTD regarding the error objects in some scenarios - it will be consistent by the time Ajv version 8 is released. Therefore it is not recommended yet to use error objects for any advanced application logic. + ### Error parameters Properties of `params` object in errors depend on the keyword that failed validation.