diff --git a/docs/json-schema.md b/docs/json-schema.md index 6b6231020..e94e3226b 100644 --- a/docs/json-schema.md +++ b/docs/json-schema.md @@ -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. The subschemas should be either inlined or included as direct references (only `$ref` keyword without any extra keywords is allowed). - schema for discriminator property in each `oneOf` subschema must be [const](#const) or [enum](#enum), with unique values across all subschemas. Not meeting any of these requirements would fail schema compilation. diff --git a/lib/vocabularies/discriminator/index.ts b/lib/vocabularies/discriminator/index.ts index f8656d8d2..ec00064f9 100644 --- a/lib/vocabularies/discriminator/index.ts +++ b/lib/vocabularies/discriminator/index.ts @@ -2,6 +2,8 @@ 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" +import {schemaHasRulesButRef} from "../../compile/util" export type DiscriminatorError = DiscrErrorObj | DiscrErrorObj @@ -62,10 +64,16 @@ 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] + let sch = oneOf[i] + if (sch?.$ref && !schemaHasRulesButRef(sch, it.self.RULES)) { + sch = resolveRef.call(it.self, it.schemaEnv, it.baseId, sch?.$ref) + if (sch instanceof SchemaEnv) sch = sch.schema + } + const propSch = sch?.properties?.[tagName] if (typeof propSch != "object") { - throw new Error(`discriminator: oneOf schemas must have "properties/${tagName}"`) + throw new Error( + `discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"` + ) } tagRequired = tagRequired && (topRequired || hasRequired(sch)) addMappings(propSch, i) diff --git a/spec/discriminator.spec.ts b/spec/discriminator.spec.ts index cf7843880..e44d60de0 100644 --- a/spec/discriminator.spec.ts +++ b/spec/discriminator.spec.ts @@ -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( @@ -97,7 +175,7 @@ describe("discriminator keyword", function () { required: ["foo"], oneOf: [{properties: {}}], }, - /discriminator: oneOf schemas must have "properties\/foo"/ + /discriminator: oneOf subschemas \(or referenced schemas\) must have "properties\/foo"/ ) })