Skip to content

Commit f3ed917

Browse files
committed
fix: recursively check for validity of discriminator
The old implementation was only checking for a single level of validity of a schema using the discriminator extension, without accounting for "unions of unions". So, for example: ```json { "discriminator": { "propertyName": "field" } "required": ["field"], "oneOf": [ { "oneOf": [ { "properties": { "field": { "const": "a" } }, "required": ["field"] }, { "properties": { "field": { "const": "b" } }, "required": ["field"] } ], "required": ["field"] }, { "properties": { "field": { "const": "c" } }, "required": ["field"] } ] } ``` This schema would fail because the compiler would complain about the first schema in `oneOf` not having the `field` property, which was defined as the `discriminator`. But it *does* have that property, just in a subschema inside another `oneOf` definition!
1 parent 142ce84 commit f3ed917

2 files changed

Lines changed: 396 additions & 14 deletions

File tree

lib/vocabularies/discriminator/index.ts

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,75 @@ const def: CodeKeywordDefinition = {
6464
const oneOfMapping: {[T in string]?: number} = {}
6565
const topRequired = hasRequired(parentSchema)
6666
let tagRequired = true
67-
for (let i = 0; i < oneOf.length; i++) {
68-
let sch = oneOf[i]
67+
addOneOfMappings(oneOf, topRequired)
68+
if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`)
69+
return oneOfMapping
70+
71+
function hasRequired({required}: AnySchemaObject): boolean {
72+
return Array.isArray(required) && required.includes(tagName)
73+
}
74+
75+
function resolveOneOfSchema(sch: AnySchemaObject): AnySchemaObject {
6976
if (sch?.$ref && !schemaHasRulesButRef(sch, it.self.RULES)) {
7077
const ref = sch.$ref
71-
sch = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref)
72-
if (sch instanceof SchemaEnv) sch = sch.schema
73-
if (sch === undefined) throw new MissingRefError(it.opts.uriResolver, it.baseId, ref)
74-
}
75-
const propSch = sch?.properties?.[tagName]
76-
if (typeof propSch != "object") {
78+
const resolved = resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref)
79+
const schema = resolved instanceof SchemaEnv ? resolved.schema : resolved
80+
if (schema === undefined) throw new MissingRefError(it.opts.uriResolver, it.baseId, ref)
81+
if (typeof schema === "object") return schema
7782
throw new Error(
7883
`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`
7984
)
8085
}
81-
tagRequired = tagRequired && (topRequired || hasRequired(sch))
82-
addMappings(propSch, i)
86+
return sch
8387
}
84-
if (!tagRequired) throw new Error(`discriminator: "${tagName}" must be required`)
85-
return oneOfMapping
8688

87-
function hasRequired({required}: AnySchemaObject): boolean {
88-
return Array.isArray(required) && required.includes(tagName)
89+
function findTagPropSchema(sch: AnySchemaObject): AnySchemaObject | undefined {
90+
const propSch = sch?.properties?.[tagName]
91+
if (typeof propSch === "object") return propSch
92+
const candidates = sch.allOf ?? sch.anyOf
93+
if (Array.isArray(candidates)) {
94+
for (const subSch of candidates) {
95+
const resolved = resolveOneOfSchema(subSch)
96+
const found = findTagPropSchema(resolved)
97+
if (found) return found
98+
}
99+
}
100+
return undefined
101+
}
102+
103+
function checkRequired(sch: AnySchemaObject, parentRequired: boolean): boolean {
104+
if (parentRequired || hasRequired(sch)) return true
105+
const candidates = sch.allOf ?? sch.anyOf
106+
if (Array.isArray(candidates)) {
107+
for (const subSch of candidates) {
108+
const resolved = resolveOneOfSchema(subSch)
109+
if (checkRequired(resolved, false)) return true
110+
}
111+
}
112+
return false
113+
}
114+
115+
function addOneOfMappings(
116+
oneOfSubschemas: AnySchemaObject[],
117+
parentRequired: boolean,
118+
topLevelIndex?: number
119+
): void {
120+
for (let i = 0; i < oneOfSubschemas.length; i++) {
121+
const mappingIndex = topLevelIndex ?? i
122+
const sch = resolveOneOfSchema(oneOfSubschemas[i])
123+
const propSch = findTagPropSchema(sch)
124+
if (typeof propSch === "object") {
125+
tagRequired = tagRequired && checkRequired(sch, parentRequired)
126+
addMappings(propSch, mappingIndex)
127+
} else if (sch?.oneOf && sch?.discriminator?.propertyName === tagName) {
128+
tagRequired = tagRequired && checkRequired(sch, parentRequired)
129+
addOneOfMappings(sch.oneOf as AnySchemaObject[], hasRequired(sch), mappingIndex)
130+
} else {
131+
throw new Error(
132+
`discriminator: oneOf subschemas (or referenced schemas) must have "properties/${tagName}"`
133+
)
134+
}
135+
}
89136
}
90137

91138
function addMappings(sch: AnySchemaObject, i: number): void {

0 commit comments

Comments
 (0)