Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OpenAPI 3.0 discriminator.mapping #2263

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/options.md
Expand Up @@ -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) .
Expand Down
31 changes: 20 additions & 11 deletions lib/compile/validate/index.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}"`)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/core.ts
Expand Up @@ -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
Expand Down
21 changes: 14 additions & 7 deletions lib/vocabularies/discriminator/index.ts
@@ -1,35 +1,37 @@
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 {schemaHasRulesButRef} from "../../compile/util"

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

const error: KeywordErrorDefinition = {
message: ({params: {discrError, tagName}}) =>
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 = {
keyword: "discriminator",
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) {
throw new Error("discriminator: requires discriminator option")
}
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)}`)
Expand Down Expand Up @@ -107,4 +109,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
53 changes: 52 additions & 1 deletion spec/discriminator.spec.ts
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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("validation with deeply referenced schemas", () => {
const schema = [
{
Expand Down