diff --git a/docs/options.md b/docs/options.md index fdce7f571..85632c445 100644 --- a/docs/options.md +++ b/docs/options.md @@ -177,6 +177,11 @@ Include the reference to the part of the schema (`schema` and `parentSchema`) an Support [discriminator keyword](./json-schema.md#discriminator) from [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md). + +To bypass the validation exception 'discriminator: mapping is not supported', the following option can be set: + +```discriminator: { strict: false }``` + ### unicodeRegExp By default Ajv uses unicode flag "u" with "pattern" and "patternProperties", as per JSON Schema spec. See [RegExp.prototype.unicode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode) . diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 15ecabd85..4b544ae79 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -2,37 +2,44 @@ import type { AddedKeywordDefinition, AnySchema, AnySchemaObject, - KeywordErrorCxt, KeywordCxtParams, + KeywordErrorCxt, } from "../../types" import type {SchemaCxt, SchemaObjCxt} from ".." import type {InstanceOptions} from "../../core" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" -import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" +import { + checkDataType, + checkDataTypes, + coerceAndCheckDataType, + DataType, + getSchemaTypes, + reportTypeError, +} from "./dataType" import {shouldUseGroup, shouldUseRule} from "./applicability" -import {checkDataType, checkDataTypes, reportTypeError, DataType} from "./dataType" import {assignDefaults} from "./defaults" import {funcKeywordCode, macroKeywordCode, validateKeywordUsage, validSchemaType} from "./keyword" -import {getSubschema, extendSubschemaData, SubschemaArgs, extendSubschemaMode} from "./subschema" -import {_, nil, str, or, not, getProperty, Block, Code, Name, CodeGen} from "../codegen" +import {extendSubschemaData, extendSubschemaMode, getSubschema, SubschemaArgs} from "./subschema" +import {_, Block, Code, CodeGen, getProperty, Name, nil, not, or, str} from "../codegen" import N from "../names" import {resolveUrl} from "../resolve" import { - schemaRefOrVal, - schemaHasRulesButRef, - checkUnknownRules, checkStrictMode, - unescapeJsonPointer, + checkUnknownRules, mergeEvaluated, + schemaHasRulesButRef, + schemaRefOrVal, + unescapeJsonPointer, } from "../util" import type {JSONType, Rule, RuleGroup} from "../rules" import { ErrorPaths, + keyword$DataError, reportError, reportExtraError, resetErrorsCount, - keyword$DataError, } from "../errors" +import {strictDiscriminatorValidation} from "../../vocabularies/discriminator" // schema compilation - generates validation function, subschemaCode (below) is used for subschemas export function validateFunctionCode(it: SchemaCxt): void { @@ -302,7 +309,9 @@ function checkKeywordTypes(it: SchemaObjCxt, ts: JSONType[]): void { if (typeof rule == "object" && shouldUseRule(it.schema, rule)) { const {type} = rule.definition if (type.length && !type.some((t) => hasApplicableType(ts, t))) { - strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`) + if (keyword !== "discriminator" || strictDiscriminatorValidation(it)) { + strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`) + } } } } diff --git a/lib/core.ts b/lib/core.ts index e41ca3e2a..3e80ff73b 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -102,7 +102,7 @@ export interface CurrentOptions { $data?: boolean allErrors?: boolean verbose?: boolean - discriminator?: boolean + discriminator?: boolean | {strict: boolean} unicodeRegExp?: boolean timestamp?: "string" | "date" // JTD only parseDate?: boolean // JTD only diff --git a/lib/vocabularies/discriminator/index.ts b/lib/vocabularies/discriminator/index.ts index 19ae6049f..26e552da1 100644 --- a/lib/vocabularies/discriminator/index.ts +++ b/lib/vocabularies/discriminator/index.ts @@ -1,8 +1,8 @@ -import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} from "../../types" +import type {AnySchemaObject, CodeKeywordDefinition, KeywordErrorDefinition} from "../../types" import type {KeywordCxt} from "../../compile/validate" import {_, getProperty, Name} from "../../compile/codegen" import {DiscrError, DiscrErrorObj} from "../discriminator/types" -import {resolveRef, SchemaEnv} from "../../compile" +import {resolveRef, SchemaEnv, SchemaObjCxt} from "../../compile" import MissingRefError from "../../compile/ref_error" import {schemaHasRulesButRef} from "../../compile/util" @@ -11,10 +11,10 @@ export type DiscriminatorError = DiscrErrorObj | DiscrErrorObj discrError === DiscrError.Tag - ? `tag "${tagName}" must be string` - : `value of tag "${tagName}" must be in oneOf`, + ? `property "${tagName}" must be string` + : `value of property "${tagName}" must be in oneOf`, params: ({params: {discrError, tag, tagName}}) => - _`{error: ${discrError}, tag: ${tagName}, tagValue: ${tag}}`, + _`{error: ${discrError}, property: ${tagName}, propertyValue: ${tag}}`, } const def: CodeKeywordDefinition = { @@ -22,7 +22,7 @@ const def: CodeKeywordDefinition = { type: "object", schemaType: "object", error, - code(cxt: KeywordCxt) { + code: function (cxt: KeywordCxt) { const {gen, data, schema, parentSchema, it} = cxt const {oneOf} = parentSchema if (!it.opts.discriminator) { @@ -30,7 +30,9 @@ const def: CodeKeywordDefinition = { } const tagName = schema.propertyName if (typeof tagName != "string") throw new Error("discriminator: requires propertyName") - if (schema.mapping) throw new Error("discriminator: mapping is not supported") + if (schema.mapping && strictDiscriminatorValidation(it)) { + throw new Error("discriminator: mapping is not supported") + } if (!oneOf) throw new Error("discriminator: requires oneOf keyword") const valid = gen.let("valid", false) const tag = gen.const("tag", _`${data}${getProperty(tagName)}`) @@ -110,4 +112,9 @@ const def: CodeKeywordDefinition = { }, } +export function strictDiscriminatorValidation(it: SchemaObjCxt): boolean { + if (it.opts.discriminator instanceof Object) return it.opts.discriminator.strict + return true +} + export default def diff --git a/spec/discriminator.spec.ts b/spec/discriminator.spec.ts index 74ba33ef0..cd93e3c21 100644 --- a/spec/discriminator.spec.ts +++ b/spec/discriminator.spec.ts @@ -19,7 +19,7 @@ describe("discriminator keyword", function () { }) function getAjvs(AjvClass: typeof AjvCore) { - return withStandalone(getAjvInstances(AjvClass, options, {discriminator: true})) + return withStandalone(getAjvInstances(AjvClass, options, {discriminator: {strict: false}})) } describe("validation", () => { @@ -159,6 +159,57 @@ describe("discriminator keyword", function () { }) }) + describe("validation with referenced schemas and mapping", () => { + const definitions1 = { + schema1: { + properties: { + foo: {const: "x"}, + a: {type: "string"}, + }, + required: ["foo", "a"], + }, + schema2: { + properties: { + foo: {enum: ["y", "z"]}, + b: {type: "string"}, + }, + required: ["foo", "b"], + }, + } + const mainSchema1 = { + type: "object", + discriminator: { + propertyName: "foo", + mapping: { + x: "#/definitions/schema1", + z: "#/definitions/schema2", + }, + }, + oneOf: [ + { + $ref: "#/definitions/schema1", + }, + { + $ref: "#/definitions/schema2", + }, + ], + } + + const schema = [{definitions: definitions1, ...mainSchema1}] + + it("should validate data", () => { + assertValid(schema, {foo: "x", a: "a"}) + assertValid(schema, {foo: "y", b: "b"}) + assertValid(schema, {foo: "z", b: "b"}) + assertInvalid(schema, {}) + assertInvalid(schema, {foo: 1}) + assertInvalid(schema, {foo: "bar"}) + assertInvalid(schema, {foo: "x", b: "b"}) + assertInvalid(schema, {foo: "y", a: "a"}) + assertInvalid(schema, {foo: "z", a: "a"}) + }) + }) + describe("schema with external $refs", () => { const schemas = { main: {