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

resolve references before validating the discriminator #1815

Merged
merged 15 commits into from Jan 15, 2022
Merged
2 changes: 1 addition & 1 deletion docs/json-schema.md
Expand Up @@ -992,7 +992,7 @@ There are following requirements and limitations of using `discriminator` keywor
- `mapping` in discriminator object is not supported.
- [oneOf](#oneof) keyword must be present in the same schema.
- discriminator property should be [requried](#required) either on the top level, as in the example, or in all `oneOf` subschemas.
- each `oneOf` subschema must have [properties](#properties) keyword with discriminator property.
- each `oneOf` subschema must have [properties](#properties) keyword with discriminator property. Here, the subschemas can also be included by reference (`$ref` keyword).
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
- schema for discriminator property in each `oneOf` subschema must be [const](#const) or [enum](#enum), with unique values across all subschemas.

Not meeting any of these requirements would fail schema compilation.
Expand Down
23 changes: 19 additions & 4 deletions lib/vocabularies/discriminator/index.ts
Expand Up @@ -2,6 +2,7 @@ import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} fro
import type {KeywordCxt} from "../../compile/validate"
import {_, getProperty, Name} from "../../compile/codegen"
import {DiscrError, DiscrErrorObj} from "../discriminator/types"
import {resolveRef, SchemaEnv} from "../../compile"
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down Expand Up @@ -62,10 +63,24 @@ const def: CodeKeywordDefinition = {
const topRequired = hasRequired(parentSchema)
let tagRequired = true
for (let i = 0; i < oneOf.length; i++) {
const sch = oneOf[i]
const propSch = sch.properties?.[tagName]
if (typeof propSch != "object") {
throw new Error(`discriminator: oneOf schemas must have "properties/${tagName}"`)
let sch = oneOf[i]
let propSch
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
if (sch?.["$ref"] && Object.keys(sch).length === 1) {
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
sch = resolveRef.call(it.self, it.schemaEnv, it.baseId, sch?.["$ref"])
if (sch instanceof SchemaEnv) {
sch = sch.schema
}
propSch = sch?.properties?.[tagName]
if (typeof propSch != "object") {
throw new Error(
`discriminator: referenced oneOf schemas must have "properties/${tagName}"`
)
}
} else {
propSch = sch.properties?.[tagName]
if (typeof propSch != "object") {
throw new Error(`discriminator: oneOf schemas must have "properties/${tagName}"`)
}
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
}
tagRequired = tagRequired && (topRequired || hasRequired(sch))
addMappings(propSch, i)
Expand Down
78 changes: 78 additions & 0 deletions spec/discriminator.spec.ts
Expand Up @@ -81,6 +81,84 @@ describe("discriminator keyword", function () {
})
})

describe("validation with referenced schemas", () => {
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"},
oneOf: [
{
$ref: "#/definitions/schema1",
},
{
$ref: "#/definitions/schema2",
},
],
}

const definitions2 = {
schema1: {
properties: {
foo: {const: "x"},
a: {type: "string"},
},
required: ["a"],
},
schema2: {
properties: {
foo: {enum: ["y", "z"]},
b: {type: "string"},
},
required: ["b"],
},
}
const mainSchema2 = {
type: "object",
discriminator: {propertyName: "foo"},
required: ["foo"],
oneOf: [
{
$ref: "#/definitions/schema1",
},
{
$ref: "#/definitions/schema2",
},
],
}

const schema = [
{definitions: definitions1, ...mainSchema1},
{definitions: definitions2, ...mainSchema2},
]

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("valid schemas", () => {
it("should have oneOf", () => {
invalidSchema(
Expand Down