From 88ec4c99b9fcce7d72b6729445f3928830acd76c Mon Sep 17 00:00:00 2001 From: Adrian Hoehn Date: Mon, 1 May 2023 17:08:58 +0200 Subject: [PATCH 1/2] Support for discriminator tag lookup in referenced schemas --- lib/vocabularies/discriminator/index.ts | 26 +++++++-- spec/discriminator.spec.ts | 78 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/lib/vocabularies/discriminator/index.ts b/lib/vocabularies/discriminator/index.ts index 98f0f8cfb..178ab88f2 100644 --- a/lib/vocabularies/discriminator/index.ts +++ b/lib/vocabularies/discriminator/index.ts @@ -1,7 +1,7 @@ import type {CodeKeywordDefinition, AnySchemaObject, KeywordErrorDefinition} from "../../types" import type {KeywordCxt} from "../../compile/validate" import {_, getProperty, Name} from "../../compile/codegen" -import {DiscrError, DiscrErrorObj} from "../discriminator/types" +import {DiscrError, DiscrErrorObj} from "./types" import {resolveRef, SchemaEnv} from "../../compile" import {schemaHasRulesButRef} from "../../compile/util" @@ -69,13 +69,31 @@ const def: CodeKeywordDefinition = { sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, sch?.$ref) if (sch instanceof SchemaEnv) sch = sch.schema } - const propSch = sch?.properties?.[tagName] - if (typeof propSch != "object") { + let propSch = sch?.properties?.[tagName] + let hasSubSchRequired = false + if (!propSch && sch?.allOf) { + let subSchObj: any = null + for (const subSch of sch.allOf) { + if (subSch?.properties) { + propSch = subSch.properties[tagName] + subSchObj = subSch + } else if (subSch?.$ref) { + subSchObj = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, subSch.$ref) + propSch = subSchObj?.properties[tagName] + } + if (propSch) { + //found discriminator mapping in one of the allOf objects, stop searching + hasSubSchRequired = hasRequired(subSchObj) + break + } + } + } + if (!propSch || typeof propSch != "object") { throw new Error( `discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"` ) } - tagRequired = tagRequired && (topRequired || hasRequired(sch)) + tagRequired = tagRequired && (topRequired || hasRequired(sch) || hasSubSchRequired) addMappings(propSch, i) } if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`) diff --git a/spec/discriminator.spec.ts b/spec/discriminator.spec.ts index 28ff12146..2522c4383 100644 --- a/spec/discriminator.spec.ts +++ b/spec/discriminator.spec.ts @@ -159,6 +159,84 @@ describe("discriminator keyword", function () { }) }) + describe("validation with referenced schemas and allOf", () => { + const definitions = { + baseObject: { + properties: { + base: {type: "string"}, + }, + required: ["base"], + }, + schema1: { + properties: { + foo: {const: "x"}, + a: {type: "string"}, + }, + required: ["foo", "a"], + }, + schema2: { + properties: { + foo: {enum: ["y", "w"]}, + b: {type: "string"}, + }, + required: ["foo", "b"], + }, + schema1Object: { + allOf: [ + { + $ref: "#/definitions/baseObject", + }, + { + $ref: "#/definitions/schema1", + }, + ], + }, + } + const mainSchema = { + type: "object", + discriminator: {propertyName: "foo"}, + oneOf: [ + { + //referenced allOf + $ref: "#/definitions/schema1Object", + }, + { + //inline allOf + allOf: [ + { + $ref: "#/definitions/baseObject", + }, + { + $ref: "#/definitions/schema2", + }, + ], + }, + { + //plain object + properties: { + base: {type: "string"}, + foo: {const: "z"}, + c: {type: "string"}, + }, + required: ["base", "foo", "c"], + }, + ], + } + + const schema = [{definitions: definitions, ...mainSchema}] + + it("should validate data", () => { + assertValid(schema, {foo: "x", a: "a", base: "base"}) + assertValid(schema, {foo: "y", b: "b", base: "base"}) + assertValid(schema, {foo: "z", c: "c", base: "base"}) + assertInvalid(schema, {}) + assertInvalid(schema, {foo: 1}) + assertInvalid(schema, {foo: "bar"}) + assertInvalid(schema, {foo: "x", b: "b"}) + assertInvalid(schema, {foo: "y", a: "a"}) + }) + }) + describe("validation with deeply referenced schemas", () => { const schema = [ { From dabb37337e2023fbb3238e1d6605523190924b25 Mon Sep 17 00:00:00 2001 From: Adrian Hoehn Date: Mon, 1 May 2023 21:33:40 +0200 Subject: [PATCH 2/2] fix for inlineRefs: false and reduced complexity --- lib/vocabularies/discriminator/index.ts | 42 +++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/vocabularies/discriminator/index.ts b/lib/vocabularies/discriminator/index.ts index 178ab88f2..5296cd3b0 100644 --- a/lib/vocabularies/discriminator/index.ts +++ b/lib/vocabularies/discriminator/index.ts @@ -1,4 +1,4 @@ -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 "./types" @@ -72,21 +72,9 @@ const def: CodeKeywordDefinition = { let propSch = sch?.properties?.[tagName] let hasSubSchRequired = false if (!propSch && sch?.allOf) { - let subSchObj: any = null - for (const subSch of sch.allOf) { - if (subSch?.properties) { - propSch = subSch.properties[tagName] - subSchObj = subSch - } else if (subSch?.$ref) { - subSchObj = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, subSch.$ref) - propSch = subSchObj?.properties[tagName] - } - if (propSch) { - //found discriminator mapping in one of the allOf objects, stop searching - hasSubSchRequired = hasRequired(subSchObj) - break - } - } + const {hasRequired, propertyObject} = mapDiscriminatorFromAllOf(propSch, sch) + hasSubSchRequired = hasRequired + propSch = propertyObject } if (!propSch || typeof propSch != "object") { throw new Error( @@ -99,6 +87,28 @@ const def: CodeKeywordDefinition = { if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`) return oneOfMapping + function mapDiscriminatorFromAllOf( + propSch: any, + sch: any + ): {hasRequired: boolean; propertyObject: any} { + let subSchObj: any = null + for (const subSch of sch.allOf) { + if (subSch?.properties) { + propSch = subSch.properties[tagName] + subSchObj = subSch + } else if (subSch?.$ref) { + subSchObj = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, subSch.$ref) + if (subSchObj instanceof SchemaEnv) subSchObj = subSchObj.schema + propSch = subSchObj?.properties?.[tagName] + } + if (propSch) { + //found discriminator mapping in one of the allOf objects, stop searching + return {hasRequired: hasRequired(subSchObj), propertyObject: propSch} + } + } + return {hasRequired: false, propertyObject: null} + } + function hasRequired({required}: AnySchemaObject): boolean { return Array.isArray(required) && required.includes(tagName) }