From c9fa02c9773fc948f35656533b134f184b6a9790 Mon Sep 17 00:00:00 2001 From: Yonathan Randolph Date: Thu, 9 May 2024 17:11:35 -0700 Subject: [PATCH 1/2] fix #2427 compileAsync a schema with discriminator and $ref Make the discriminator code generation throw MissingRefError when the $ref does not synchronously resolve so that compileAsync can loadSchema and retry. --- lib/vocabularies/discriminator/index.ts | 5 +- spec/discriminator.spec.ts | 61 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/vocabularies/discriminator/index.ts b/lib/vocabularies/discriminator/index.ts index 98f0f8cfb..19ae6049f 100644 --- a/lib/vocabularies/discriminator/index.ts +++ b/lib/vocabularies/discriminator/index.ts @@ -3,6 +3,7 @@ import type {KeywordCxt} from "../../compile/validate" import {_, getProperty, Name} from "../../compile/codegen" import {DiscrError, DiscrErrorObj} from "../discriminator/types" import {resolveRef, SchemaEnv} from "../../compile" +import MissingRefError from "../../compile/ref_error" import {schemaHasRulesButRef} from "../../compile/util" export type DiscriminatorError = DiscrErrorObj | DiscrErrorObj @@ -66,8 +67,10 @@ const def: CodeKeywordDefinition = { for (let i = 0; i < oneOf.length; i++) { let sch = oneOf[i] if (sch?.$ref && !schemaHasRulesButRef(sch, it.self.RULES)) { - sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, sch?.$ref) + const ref = sch.$ref + sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref) if (sch instanceof SchemaEnv) sch = sch.schema + if (sch === undefined) throw new MissingRefError(it.opts.uriResolver, it.baseId, ref) } const propSch = sch?.properties?.[tagName] if (typeof propSch != "object") { diff --git a/spec/discriminator.spec.ts b/spec/discriminator.spec.ts index 28ff12146..ea8a11bfe 100644 --- a/spec/discriminator.spec.ts +++ b/spec/discriminator.spec.ts @@ -159,6 +159,67 @@ describe("discriminator keyword", function () { }) }) + describe("schema with external $refs", () => { + const schemas = { + main: { + type: "object", + discriminator: {propertyName: "foo"}, + required: ["foo"], + oneOf: [ + { + $ref: "schema1", + }, + { + $ref: "schema2", + }, + ], + }, + schema1: { + type: "object", + properties: { + foo: {const: "x"}, + }, + }, + schema2: { + type: "object", + properties: { + foo: {enum: ["y", "z"]}, + }, + }, + } + + const data = {foo: "x"} + const badData = {foo: "w"} + + it("compile should resolve each $ref to a schema that was added with addSchema", () => { + const options = { + discriminator: true, + } + const ajv = new _Ajv(options) + ajv.addSchema(schemas.main, "https://host/main") + ajv.addSchema(schemas.schema1, "https://host/schema1") + ajv.addSchema(schemas.schema2, "https://host/schema2") + + const validate = ajv.compile({$ref: "https://host/main"}) + assert.strictEqual(validate(data), true) + assert.strictEqual(validate(badData), false) + }) + it("compileAsync should loadSchema each $ref", async () => { + const options = { + discriminator: true, + async loadSchema(url) { + if (!url.startsWith("https://host/")) return undefined + const name = url.substring("https://host/".length) + return schemas[name] + }, + } + const ajv = new _Ajv(options) + const validate = await ajv.compileAsync({$ref: "https://host/main"}) + assert.strictEqual(validate(data), true) + assert.strictEqual(validate(badData), false) + }) + }) + describe("validation with deeply referenced schemas", () => { const schema = [ { From 6c3899f50881e6675c5bb09553c3d8333b894704 Mon Sep 17 00:00:00 2001 From: Jason Green Date: Sat, 11 May 2024 13:37:29 +0100 Subject: [PATCH 2/2] test: fix errors in test --- spec/discriminator.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/discriminator.spec.ts b/spec/discriminator.spec.ts index ea8a11bfe..74ba33ef0 100644 --- a/spec/discriminator.spec.ts +++ b/spec/discriminator.spec.ts @@ -192,10 +192,10 @@ describe("discriminator keyword", function () { const badData = {foo: "w"} it("compile should resolve each $ref to a schema that was added with addSchema", () => { - const options = { + const opts = { discriminator: true, } - const ajv = new _Ajv(options) + const ajv = new _Ajv(opts) ajv.addSchema(schemas.main, "https://host/main") ajv.addSchema(schemas.schema1, "https://host/schema1") ajv.addSchema(schemas.schema2, "https://host/schema2") @@ -205,15 +205,15 @@ describe("discriminator keyword", function () { assert.strictEqual(validate(badData), false) }) it("compileAsync should loadSchema each $ref", async () => { - const options = { + const opts = { discriminator: true, - async loadSchema(url) { + loadSchema(url) { if (!url.startsWith("https://host/")) return undefined const name = url.substring("https://host/".length) return schemas[name] }, } - const ajv = new _Ajv(options) + const ajv = new _Ajv(opts) const validate = await ajv.compileAsync({$ref: "https://host/main"}) assert.strictEqual(validate(data), true) assert.strictEqual(validate(badData), false)